「Dockerを理解する」という出版物に触発され、Webアプリケーションを起動するためのドッカーの周りの松葉杖の小さな例です。
さまざまなストラップテクノロジーを試しましたが、一部(fig)は少し使いにくいように見え、一部(kubernetis、mesos)は抽象的で複雑に見えます。
私の構成には複数のマシンがあり、マシン上でさまざまなWebアプリケーションが実行されていますが、その一部はローカルストレージを必要とします。 基本的なスキームとして、2つのフロントエンドと1つのバックエンドの構成を採用し、必要に応じてceph(FS)がバックエンドのデータローミングを提供します。
車にはプライベートネットワークインターフェイスがあります。 フロントエンドにはパブリックなものもあります。
構成の日に、多くのetcd + skydns(サービス検出)、runit(コンテナーの状態の監視)、およびansible(構成)を使用します。 以下に、ansibleモジュールのコードを示します。これについて説明します。
たくさんのコード
#!/usr/bin/env python import os, sys from string import Template def on_error(msg): def wrap(f): def wrapped(self, module): try: return f(self, module) except Exception, e: module.fail_json(msg="%s %s: %s" % (msg, self.name, str(e))) return wrapped return wrap class Service: SERVICE_PREFIX = 'docker-' SERVICES_DIR = '/etc/sv' RUNNING_SERVICES_DIR = '/etc/service' def __init__(self, name, image, args, announce, announce_as, port): self.name = name self.image = image if args is not None: self.args = args else: self.args = '' self.announce = announce self.announce_as = announce_as self.port = port def _needs_etcd(self): return self.announce is not None def _service_name(self): return self.SERVICE_PREFIX + self.name def _root_service_dir(self): return os.path.join(self.SERVICES_DIR, self._service_name()) def _announced_service_dir(self): return os.path.join(self._root_service_dir(), 'services', 'service') def _etcd_service_dir(self): return os.path.join(self._root_service_dir(), 'services', 'announce') def _run_service_link(self): return os.path.join(self.RUNNING_SERVICES_DIR, self._service_name()) def _root_run_file(self): return os.path.join(self._root_service_dir(), 'run') def _announced_service_run_file(self): return os.path.join(self._announced_service_dir(), 'run') def _etcd_run_file(self): return os.path.join(self._etcd_service_dir(), 'run') def exists(self): return os.path.isdir(self._root_service_dir()) def scheduled_to_run(self): return os.path.exists(self._run_service_link()) @on_error("Error starting service") def start(self, module): if self._needs_update(module): self.install(module) if self.scheduled_to_run(): return False os.symlink(self._root_service_dir(), self._run_service_link()) return True @on_error("Error stopping service") def stop(self, module): if not self.scheduled_to_run(): return False os.unlink(self._run_service_link()) return True @on_error("Error installing service") def install(self, module): if self._needs_update(module): self.stop(module) self.remove(module) self._create_service(module) return True else: return False @on_error("Error creating service") def _create_service(self, module): self._create_service_dirs(module) self._write_run_file(self._root_run_file(), self._render_root_run()) if self._needs_etcd(): self._write_run_file(self._announced_service_run_file(), self._render_service_run()) self._write_run_file(self._etcd_run_file(), self._render_etcd_run()) def _write_run_file(self, name, content): f = open(name, 'w') f.write(content) os.fchmod(f.fileno(), 0755) f.close() @on_error("Error verifying service existence") def _needs_update(self, module): if self.exists(): if os.path.exists(self._root_run_file()): root_run = self._render_root_run() curr_run = open(self._root_run_file()).read() if root_run != curr_run: return True if self._needs_etcd(): if os.path.exists(self._announced_service_run_file()): service_run = self._render_service_run() curr_run = open(self._announced_service_run_file()).read() if service_run != curr_run: return True if os.path.exists(self._etcd_run_file()): etcd_run = self._render_etcd_run() curr_run = open(self._etcd_run_file()).read() if etcd_run != curr_run: return True else: return True else: return True else: return True else: return True return False @on_error("Error creating service directory") def _create_service_dirs(self, module): os.mkdir(self._root_service_dir(), 0755) if self._needs_etcd(): os.mkdir(os.path.join(self._root_service_dir(), 'services'), 0755) os.mkdir(self._announced_service_dir(), 0755) os.mkdir(self._etcd_service_dir(), 0755) @on_error("Error removing service") def remove(self, module): if not self.exists(): return False if self.scheduled_to_run(): self.stop(module) from shutil import rmtree rmtree(self._root_service_dir()) return True def _render_root_run(self): if self._needs_etcd(): return self._render_runsv_run() else: return self._render_service_run() def _render_service_run(self): args = self.args if self.announce: if self.port is not None: port = self.port else: port = self.announce if self.announce_as != 'container': args += " -p $ANNOUNCE_IP:" + self.announce + ":" + port return Template("""#!/bin/bash CONTAINER_NAME=$name ifconfig eth1 >/dev/null 2>&1 if [[ $$? -eq 0 ]]; then PUBILC_IF=eth0 PRIVATE_IF=eth1 else PUBILC_IF=eth0 PRIVATE_IF=eth0 fi case "$announce_as" in public) ANNOUNCE_IP="`ifconfig $$PUBILC_IF | sed -En 's/127.0.0.1//;s/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\\2/p'`" ;; private) ANNOUNCE_IP="`ifconfig $$PRIVATE_IF | sed -En 's/127.0.0.1//;s/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\\2/p'`" ;; *) ANNOUNCE_IP="" ;; esac docker inspect $$CONTAINER_NAME|grep State >/dev/null 2>&1 if [ $$? -eq 0 ]; then docker rm $$CONTAINER_NAME || { echo "cannot remove container $$CONTAINER_NAME"; exit 1; } fi docker pull $image exec docker run \ -i --rm \ --name $$CONTAINER_NAME \ --hostname "`hostname`-$name" \ $args \ $image """).substitute(name=self.name, image=self.image, args=args, announce_as=self.announce_as) def _render_runsv_run(self): return """#!/bin/bash runsvdir -P services & RUNSVPID=$! trap "{ sv stop `pwd`/services/*; sv wait `pwd`/services/*; kill -HUP $RUNSVPID ; exit 0; }" SIGINT SIGTERM wait """ def _render_etcd_run(self): return Template("""#!/bin/bash ETCD="http://192.0.2.1:4001" DOMAIN="com/example/prod/s/$name/`hostname`" ifconfig eth1 >/dev/null 2>&1 if [[ $$? -eq 0 ]]; then PUBILC_IF=eth0 PRIVATE_IF=eth1 else PUBILC_IF=eth0 PRIVATE_IF=eth0 fi case "$announce_as" in public) ANNOUNCE_IP="`ifconfig $$PUBILC_IF | sed -En 's/127.0.0.1//;s/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\\2/p'`" ;; private) ANNOUNCE_IP="`ifconfig $$PRIVATE_IF | sed -En 's/127.0.0.1//;s/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\\2/p'`" ;; *) ANNOUNCE_IP="" ;; esac enable -f /usr/lib/sleep.bash sleep trap "{ curl -L "$$ETCD/v2/keys/skydns/$$DOMAIN" -XDELETE ; exit 0; }" SIGINT SIGTERM while true; do if [[ "$announce_as" == "container" ]]; then ANNOUNCE_IP="`docker inspect --format '{{ .NetworkSettings.IPAddress }}' $name`" fi curl -L "$$ETCD/v2/keys/skydns/$$DOMAIN" -XPUT -d value="{\\"host\\": \\"$$ANNOUNCE_IP\\", \\"port\\": $port}" -d ttl=60 >/dev/null 2>&1 sleep 45 done""").substitute(name=self.name, port=self.announce, announce_as=self.announce_as) def main(): module = AnsibleModule( argument_spec = dict( state = dict(required=True, choices=['present', 'absent', 'enabled', 'disabled']), name = dict(required=True), image = dict(required=True), args = dict(default=None), announce = dict(default=None), announce_as = dict(default='private', choices=['public', 'private', 'container']), port = dict(default=None) ) ) state = module.params['state'] name = module.params['name'] image = module.params['image'] args = module.params['args'] announce = module.params['announce'] announce_as = module.params['announce_as'] port = module.params['port'] svc = Service(name, image, args, announce, announce_as, port) if state == 'present': module.exit_json(changed=svc.install(module)) if state == 'absent': module.exit_json(changed=svc.remove(module)) if state == 'enabled': module.exit_json(changed=svc.start(module)) if state == 'disabled': module.exit_json(changed=svc.stop(module)) module.fail_json(msg='Unexpected position reached') sys.exit(0) from ansible.module_utils.basic import * main()
新しいサービスを開始するとどうなるか見てみましょう。 たとえば、influxdbを実行します。
ansible -i hosts node-back-1 -s -m rundock -a 'state=enabled name=influxdb image="registry.s.prod.example.com:5000/influxdb:latest" args="--volumes-from data.influxdb -p $PRIVATE_IP:8083:8083" announce=8086 port=8086'
Ansibleは、runitの新しいタスクをマシンに追加します。これには、コンテナとアナウンスメントという2つのサブタスクが含まれます。
$ cat /etc/sv/docker-influxdb/services/service/run #!/bin/bash CONTAINER_NAME=influxdb INTERFACE=eth0 PRIVATE_IP="`ifconfig $INTERFACE | sed -En 's/127.0.0.1//;s/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\2/p'`" docker inspect $CONTAINER_NAME|grep State >/dev/null 2>&1 if [ $? -eq 0 ]; then docker rm $CONTAINER_NAME || { echo "cannot remove container $CONTAINER_NAME"; exit 1; } fi docker pull registry.s.prod.example.com:5000/influxdb:latest exec docker run -i --rm --name $CONTAINER_NAME --hostname "`hostname`-influxdb" --volumes-from data.influxdb -p $PRIVATE_IP:8083:8083 -p $PRIVATE_IP:8086:8086 registry.s.prod.example.com:5000/influxdb:latest
runitは古いコンテナを強制終了し、新しいコンテナをダウンロードして、インタラクティブにdockerを起動します。 コンテナが停止すると、runitはコンテナを再起動します。
data.influxdb
コンテナでは、influxがデータを保存するFSへの途中で
data.influxdb
行われました。
2番目のサービス:
$ cat /etc/sv/docker-influxdb/services/announce/run #!/bin/bash ETCD="http://192.0.2.1:4001" DOMAIN="com/example/prod/s/influxdb/`hostname`" INTERFACE=eth0 enable -f /usr/lib/sleep.bash sleep trap "{ curl -L "$ETCD/v2/keys/skydns/$DOMAIN" -XDELETE ; exit 0; }" SIGINT SIGTERM while true; do PRIVATE_IP="`ifconfig $INTERFACE | sed -En 's/127.0.0.1//;s/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\2/p'`" curl -L "$ETCD/v2/keys/skydns/$DOMAIN" -XPUT -d value="{\"host\": \"$PRIVATE_IP\", \"port\": 8086}" -d ttl=60 >/dev/null 2>&1 sleep 45
bashモジュールは組み込みコマンドとしてsleepを追加し、bashはドメインのレコードを更新し、influxdbはnode-back-1.influxdb.s.prod.example.comで利用可能になります。
松葉杖 :コンテナがクラッシュループに入った場合でもアナウンスは生きているため、良い方法では、アナウンスはコンテナ内から行う必要があります。
フロントエンドのgrafanaを固定します。
ansible -i hosts node-back-1 -s -m rundock -a 'state=enabled name=grafana image="tutum/grafana:latest" args="-e INFLUXDB_HOST=influxdb.s.prod.example.com -e INFLUXDB_PORT=8086 -e INFLUXDB_NAME=metrics -e INFLUXDB_USER=metrics -e INFLUXDB_PASS=metrics -e HTTP_PASS=metrics -e INFLUXDB_IS_GRAFANADB=true" announce=8087 port=80'
ここで、ポートとアナウンスは異なります。標準コンテナはポート80でgrafanaを提供し、8087に配布するためです。
最後に、nginxのアップストリーム:
upstream docker_grafana { server grafana.s.prod.example.com:8087; keepalive 512; }
松葉杖 :手で釘付けされるポート。 良い方法では、 このようなものがnginxにSRVレコードの使用方法を教えるかもしれません。
ソリューションの安定性について話してください。
フロントエンド。 フロントエンドが停止した場合、DNSレコードを更新する必要があります。 私たちはしばらく嘘をつき、悲しみを感じます。
発見。 etcd / skydnsは、コンセンサスで適切に組み立てられている場合、一般に殺すのが困難です。
バックエンドサービス。 複数のバックエンドを実行できるように、マシン名なしでサービスを解決します。 skydnsは、負荷を分散するか、停止したサービスをすばやく置き換えます。
ファイルシステム。 理想的な世界では、私たちは完全に不変の状態にありますが、人生ではすべてが悲しいです。 レプリケーションを理解するデータベースは、ローカルディスクまたは通常の
--volume
ストレージを保持できます。 コンテナ間で何かを配布する必要がある場合、cefが機能します(paxosも、殺すのは困難です)。