Dockerを使って開発していると、本番環境でもDockerを使ってみたいなぁという気持ちになります。ただ、本番環境でDockerを使うのは、個人的にまだまだハードルが高そうな印象です。マルチホスト間のネットワークはどうするか?分散システムのクラスタリングはどうするか?コンテナのスケジューリングは?監視は?ログは?デプロイの仕組みは?負荷が高い時の対応はどうするのか?セキュリティは?など、運用面で色々と考えなければなりません。さらにDocker界隈のツールは様々なツールが存在していてどれから手をつけていいのか選択に困ります。最初はなるべく小さく始めたいところです。なので、今回、本番環境を意識したDocker環境をより少ないツールで作れるかどうか試してみました。ひとまず、クラスタ組んで、マルチホストネットワーキング機能が試せるところまで。使ったツールは以下になります。
- CoreOS - DockerホストOS(betaチャンネル 899.3.0)
- cloud-config - OS初期設定
- etcd - 分散KVS + クラスタリング
- Docker - v1.9.3
- Docker Swarm - コンテナスオーケストレーション
- Terraform - インフラデプロイ
今回試した環境のterraformファイルは、githubにあげてますので、よかったら参考にどうぞ。
なお、プライベートな環境下で構築を試したのでProxyの設定をもろもろしていますが、参考にする場合は適当に読み替えていただければと思います。
TerraformによるCoreOSのデプロイとetcdの設定
Dockerを本番環境で使う場合、複数ホストのコンテナ間通信をどうするかというのは気になる問題ですが、いくつか方法が存在します。
Dockerのバージョン1.9からマルチホストネットワーク機能が正式に導入されたので、今回はそれを使ってみます。マルチホストネットワーク機能を使うためには、以下のような条件が必要です。今回、極力カーネルコンパイルとかはしたくなかったので、条件を満たすためにCoreOSを選択しました。
- DockerホストOSのカーネルが3.16かそれ以降
- Dockerがサポートしている分散KVS(Consul, Etcd, ZooKeeper)が利用できること
まず、CoreOSサーバ3台を作りetcdクラスタを組みます。Dockerのドキュメントではdocker-machineを使った手順なんですが、普段からTerraformでインフラ管理してるので今回Terraformを使います。Terraformの設定ファイルは以下になります。個々のパラメータは適当に読み替えてください。
## main.tf provider "openstack" { domain_name = "default" tenant_name = "project-1" auth_url = "<keystone endpoint>" } variable "servers" { default = "test-1,test-2,test-3" } resource "null_resource" "discovery_url_template" { provisioner "local-exec" { command = "curl -s 'https://discovery.etcd.io/new?size=${length(split(",", var.servers))}' > discovery_url" } } resource "template_file" "discovery_url" { template = "discovery_url" depends_on = [ "null_resource.discovery_url_template" ] } resource "template_file" "cloud-init" { count = "${length(split(",", var.servers))}" template = "${file("cloud-config.yml.tpl")}" vars { hostname = "${element(split(",", var.servers), count.index)}" discovery_url = "${template_file.discovery_url.rendered}" } } resource "openstack_compute_instance_v2" "coreos" { count = "${length(split(",", var.servers))}" name = "${element(split(",", var.servers), count.index)}" image_name = "coreos-stable-899-3-0" flavor_name = "flavor-1" region = "region-1" network { name = "network-1" } key_pair = "core" security_groups = ["default"] user_data = "${element(template_file.cloud-init.*.rendered, count.index)}" }
CoreOSは、OS初期設定をcloud-config.ymlで行うのが標準仕様になってます。OpenStack上でCoreOSを立てる時は、user_data
かconfig_drive
にcloud-configの設定を渡すことで、インスタンス起動時に初期設定が行われます。cloud-config.ymlを動的に設定したいので、Terraformのtemplate_file
リソースを使います。cloud-config.yml.tplファイルは以下のようになります。
#cloud-config hostname: ${hostname} coreos: etcd2: discovery: ${discovery_url} discovery-proxy: http://proxy.example.com:1234 <- Discovery URLにアクセスするためのproxyの設定 advertise-client-urls: http://$private_ipv4:2379,http://$private_ipv4:4001 initial-advertise-peer-urls: http://$private_ipv4:2380 listen-client-urls: http://0.0.0.0:2379,http://0.0.0.0:4001 listen-peer-urls: http://$private_ipv4:2380 units : - name: etcd2.service command: start - name: docker-tcp.socket command: start enable: true content: | [Unit] Description=Docker Socket for the API [Socket] ListenStream=2375 BindIPv6Only=both Service=docker.service [Install] WantedBy=sockets.target - name: docker.service drop-ins: - name: 20-http-proxy.conf content: | [Service] Environment="HTTP_PROXY=http://proxy.example.com:1234" command: restart
etcdクラスタを組む際、各サーバで動かすetcd同士が同じクラスタに所属することを識別させるための情報(discovery token)が必要になります。手動でやる場合、以下のURLにアクセスしdiscovery tokenを取得してetcdの設定に使用するんですが、今回はTerraformのnull_resource
を使ってtokenを取得し、cloud-config.ymlを動的に生成するようにしています。
$ curl -s 'https://discovery.etcd.io/new?size=3'
以上のファイルの準備が整ったらterraform applyを実行します。インスタンスが立ち、etcdクラスタが作られます。etcdクラスタが組めているか、以下のコマンドで確認します。
$ etcdctl cluster-health member 39da9b2c59694a93 is healthy: got healthy result from http://xxx.xxx.xxx.xxx:2379 member 53d999311a07d120 is healthy: got healthy result from http://yyy.yyy.yyy.yyy:2379 member d4822f0cf45608fd is healthy: got healthy result from http://zzz.zzz.zzz.zzz:2379 cluster is healthy
Docker Swarmクラスタを組む
次にDocker Swarmクラスタを組みます。Docker Swarmは、Dockerホストをクラスタリングし、Dockerコンテナを起動する時にどのホストで起動させるのかをスケジュールするツールです。Docker社が提供してます。
Docker Swarmは、AgentとManagerの2つの構成になっていて、それぞれDockerコンテナとして動かします。また、Swarm Managerは1台あればSwarmクラスタが組めますが、Swarm Managerが落ちるとDocker Swarm自体が使えなくなってしまうので、Swarm Managerを複数(プライマリとレプリカの構成で)動かします。その構成の場合、サービスディスカバリーとしてConsul, Etcd, ZooKeeperのどれかを使う必要があります。今回は、CoreOSに標準で組み込まれているEtcdを使います。
以下のコマンドで、Docker Swarm Agentを起動します。
$ docker run -d --name=swarm-agent swarm join --addr=$(cut -d'=' -f2 /etc/environment):2375 etcd://$(cut -d'=' -f2 /etc/environment):2379/nodes
次に、Docker Swarm Managerを起動します。4000番ポートをDocker Swarm Managerのポートにしています。
$ docker run -d -p $(cut -d'=' -f2 /etc/environment):4000:4000 --name=swarm-manager swarm manage -H :4000 --replication --advertise $(cut -d'=' -f2 /etc/environment):4000 etcd://$(cut -d'=' -f2 /etc/environment):2379/nodes
4000番ポートでdocker infoを実行するとクラスタの情報がわかります。
## Host A $ docker -H $(cut -d'=' -f2 /etc/environment):4000 info Containers: 6 Images: 3 Role: primary Strategy: spread Filters: health, port, dependency, affinity, constraint Nodes: 3 xxx: xxx.xxx.xxx.xxx:2375 └ Status: Healthy └ Containers: 2 └ Reserved CPUs: 0 / 4 └ Reserved Memory: 0 B / 16.46 GiB └ Labels: executiondriver=native-0.2, kernelversion=4.3.3-coreos-r1, operatingsystem=CoreOS 899.3.0, storagedriver=overlay yyy: yyy.yyy.yyy.yyy:2375 └ Status: Healthy └ Containers: 2 └ Reserved CPUs: 0 / 4 └ Reserved Memory: 0 B / 16.46 GiB └ Labels: executiondriver=native-0.2, kernelversion=4.3.3-coreos-r1, operatingsystem=CoreOS 899.3.0, storagedriver=overlay zzz: zzz.zzz.zzz.zzz:2375 └ Status: Healthy └ Containers: 2 └ Reserved CPUs: 0 / 4 └ Reserved Memory: 0 B / 16.46 GiB └ Labels: executiondriver=native-0.2, kernelversion=4.3.3-coreos-r1, operatingsystem=CoreOS 899.3.0, storagedriver=overlay CPUs: 12 Total Memory: 49.38 GiB Name: be07ab661423 ## Host B $ docker -H $(cut -d'=' -f2 /etc/environment):4000 info Containers: 6 Images: 3 Role: replica Primary: xxx.xxx.xxx.xxx:4000 Strategy: spread Filters: health, port, dependency, affinity, constraint Nodes: 3 ... ...
無事にDocker Swarmが組めているのがわかります。試しにSwarm Managerを落としてみると、primaryが別なホストに移るのがわかります。
## Host A $ docker stop swarm-manager ## Host B $ docker -H $(cut -d'=' -f2 /etc/environment):4000 info Containers: 6 Images: 3 Role: primary Strategy: spread Filters: health, port, dependency, affinity, constraint Nodes: 3 ... ...
ちなみに、環境変数DOCKER_HOST
を設定しておくとオプションを省略できて、シングルホストの時のように使えます。
$ export DOCKER_HOST=$(cut -d'=' -f2 /etc/environment):4000 $ docker info Containers: 6 Images: 3 Role: primary Strategy: spread Filters: health, port, dependency, affinity, constraint Nodes: 3 ... ...
また、Dockerコンテナをsystemdから起動できるようにする場合は、以下のような記述をcloud-config.yml.tplに追記します。
- name: swarm-agent.service command: start content: | [Unit] Description=Docker Swarm Agent Container After=docker.service Requires=docker.service [Service] TimeoutStartSec=0 Restart=always ExecStartPre=-/usr/bin/docker stop swarm-agent ExecStartPre=-/usr/bin/docker rm -f swarm-agent ExecStartPre=-/usr/bin/docker pull swarm ExecStart=/usr/bin/docker run --name=swarm-agent swarm join --addr=$private_ipv4:2375 etcd://$private_ipv4:2379/nodes [Install] WantedBy=multi-user.target - name: swarm-manager.service command: start content: | [Unit] Description=Docker Swarm Manager Container After=docker.service Requires=docker.service [Service] TimeoutStartSec=0 Restart=always ExecStartPre=-/usr/bin/docker stop swarm-manager ExecStartPre=-/usr/bin/docker rm -f swarm-manager ExecStartPre=-/usr/bin/docker pull swarm ExecStart=/usr/bin/docker run -p $private_ipv4:4000:4000 --name=swarm-manager swarm manage -H :4000 --replication --advertise $private_ipv4:4000 etcd://$private_ipv4:2379/nodes [Install] WantedBy=multi-user.target
こうすることでterraform applyだけで、Docker Swarmクラスタまで組めるようになります。
マルチホストネットワーク機能を試す
Docker Swarmクラスタが組めたので(ようやく)、次にマルチホストネットワーク機能を試します。
マルチホストネットワーク機能は、overlayドライバを使うことで仮想ネットワークを作ることができます。以下のコマンドでoverlayネットワークを作ります。Docker Swarmを使ってる場合、overlayドライバがデフォルトなので--driverオプションは省略可能です。
$ docker network create --driver overlay backend
ここで以下のようなメッセージが出る場合は、Dockerのクラスタ設定--cluster-advertise
と--cluster-store
が抜けているので、設定を追加します。
$ docker network create backend Error response from daemon: 500 Internal Server Error: failed to parse pool request for address space "GlobalDefault" pool "" subpool "": cannot find address space GlobalDefault (most likely the backing datastore is not configured)
以下のファイルから、Dockerオプションを設定することができます。ファイルを作ったらdocker.service
を再起動します。
$ sudo vi /etc/systemd/system/docker.service.d/30-custom.conf [Service] Environment="DOCKER_OPTS=--cluster-advertise eth0:2375 --cluster-store etcd://xxx.xxx.xxx.xxx:2379" $ sudo systemctl daemon-reload $ sudo systemctl restart docker
が、再度試したところ、また違うエラーが・・・。
$ docker network create backend Error response from daemon: 500 Internal Server Error: error getting pools config from store during init: could not get pools config from store: client: response is invalid json. The endpoint is probably not valid etcd cluster endpoint.
今回、だいぶここでハマったんですが、Docker APIの通信がProxyを介してしまってうまくいかなかったようです。NO_PROXY
にDockerホストのIPを全部並べてDockerホスト間はProxyさせないようにします。
$ cat /etc/systemd/system/docker.service.d/20-http-proxy.conf [Service] Environment="HTTP_PROXY=http://proxy.example.com:1234" Environment="NO_PROXY=localhost,127.0.0.1,xxx.xxx.xxx.xxx,yyy.yyy.yyy.yyy,zzz.zzz.zzz.zzz"
再度docker.service
を再起動させて、無事にネットワークができました。
$ docker network create backend 856e8f470224627390abaaa31db868bd01ba7423e0bf7ea5b020f7e8e0336b34 $ docker network ls NETWORK ID NAME DRIVER 856e8f470224 backend overlay f52c2ae81a2a xxx/bridge bridge f9c89af96fad yyy/none null ... ...
では、コンテナを2つ立ち上げて疎通が確認できるか試してみます。
$ docker run -itd --net=backend --name test1 --hostname test1 ubuntu /bin/bash $ docker run -itd --net=backend --name test2 --hostname test2 ubuntu /bin/bash $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c1b2cd585175 ubuntu "/bin/bash" 5 minutes ago Up 49 seconds xxx/test2 01f8c3067398 ubuntu "/bin/bash" 5 minutes ago Up 5 minutes zzz/test1
docker ps
コマンドから、異なるホストでコンテナ2つが起動してるのがわかります。
$ docker exec -it zzz/test1 /bin/bash root@test1:/# ip a 1: lo: <LOOPBACK,UP,LOWER_UP> 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 27: eth0@if28: <BROADCAST,MULTICAST,UP,LOWER_UP> 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 29: eth1@if30: <BROADCAST,MULTICAST,UP,LOWER_UP> 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についている10.0.0.0/24のネットワークがoverlayネットワークです。
root@test1:/# ping test2 PING test2 (10.0.0.3) 56(84) bytes of data. 64 bytes from test2 (10.0.0.3): icmp_seq=1 ttl=64 time=0.950 ms 64 bytes from test2 (10.0.0.3): icmp_seq=2 ttl=64 time=0.544 ms ^C --- test2 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1001ms rtt min/avg/max/mdev = 0.544/0.747/0.950/0.203 ms root@test1:/#
おぉ、疎通も無事確認できました!
ところで、コンテナ同士の名前解決はどうやってるんでしょうか。/etc/resolv.conf
などをみてみましたが、どうやら/etc/hosts
でやっているようです。
root@test1:/# cat /etc/hosts 10.0.0.2 test1 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 test2 10.0.0.3 test2.backend root@test1:/#
試しに、新しくtest3を作ってみると/etc/hosts
に新しい設定が追加されてました。
root@test1:/# cat /etc/hosts 10.0.0.2 test1 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 test2 10.0.0.3 test2.backend 10.0.0.4 test3 10.0.0.4 test3.backend root@test1:/#
まとめ
Dockerを本番環境で使うために、CoreOSでDocker Swarmクラスタを組んでマルチホストネットワーク機能を試してみました。DockerをProxy環境下で動かす場合は、いろんな場所にproxy設定を記述しないといけないのがアレですね。ここまで動かすのにProxyのおかげでだいぶ消耗しました・・・。あと、コンテナを立ち上げるのにdocker runコマンド経由なのが少し煩わしく感じました。その辺はスクリプトにするか、もう少し宣言的に出来ないかと思うのでdocker-composeやnomadなどを検討する必要がありそうです。でも、まぁ、ひとまず、環境ができてよかったです。
プログラマのためのDocker教科書 インフラの基礎知識&コードによる環境構築の自動化
- 作者: WINGSプロジェクト阿佐志保,山田祥寛
- 出版社/メーカー: 翔泳社
- 発売日: 2015/11/20
- メディア: 大型本
- この商品を含むブログ (1件) を見る
Docker実践入門――Linuxコンテナ技術の基礎から応用まで (Software Design plus)
- 作者: 中井悦司
- 出版社/メーカー: 技術評論社
- 発売日: 2015/09/26
- メディア: 大型本
- この商品を含むブログ (2件) を見る
Docker 実践ガイド (impress top gear)
- 作者: 古賀政純
- 出版社/メーカー: インプレス
- 発売日: 2015/12/17
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る