2016-10-18

О Docker Swarm

Swarm — это рой.
В частности, насекомых. Пчёл, например. Но лично у меня это слово ассоциируется со стаей зергов. Помните, такие милые и очаровательные букашки? Ну а в Docker тоже есть свой Swarm — рой Docker Engine.
Zerg Swarm
Одиночный Docker Engine запускает контейнеры на локалохосте, из образов, либо собранных тоже на локалхосте, либо выкачанных с Docker Hub.
Docker Swarm — это кластер Докеров. Множество хостов, на каждом из которых запущен отдельный Docker Engine, объединены под общим управлением, и выглядят как один большой Докер. Хосты могут добавляться в кластер, и вычислительная ёмкость вашего большого Докера будет тоже увеличена.
Поднять свою стаю Докеров довольно легко. Главный момент: компоненты кластера — это тоже докер контейнеры. Вообще складывается ощущение, что в мире Докера образы используются как обычные приложения: можно и нужно запускать одни и те же образы с разными параметрами, чтобы получить нужный результат.
Docker Swarm
По умолчанию Docker Engine, то бишь демон Docker, управляется через локальный сокет /var/run/docker.sock. Для Swarm демоны должны управляться удалённо, через TCP. Поэтому демону нужно добавить параметр запуска:
-H tcp://0.0.0.0:2275
Демон будет слушать порт 2275, без TLS шифрования. В Ubuntu с Upstart этот параметр задаётся в файле /etc/default/docker, дописать в переменную DOCKER_OPTS.
docker cli
Управлять Swarm будет Swarm Manager. Это, внезапно, контейнер, запущенный из образа swarm с параметром manage.
Можно для этого образа ручками сделать docker run. Можно сочинить docker-compose.yml. Но я, так как дело будет происходить на кучке хостов кластера, предпочёл запользовать Ansible. Модуль docker_service работает вполне себе ничего. Он дёргает docker-compose на целевой машине. А описание самого композа можно задать прямо в ансибловой задаче (и там и там, таки, YAML), со всей мощью шаблонов, конечно.
Получается, что через Ansible Docker Swarm Manager можно запустить как-то так.
- name: start swarm manager
  run_once: true
  become: yes
  docker_service:
    project_name: swarm
    definition:
      version: '2'
      services:
        manager:
          image: swarm
          command: 'manage -H :3375 --advertise {{ inventory_hostname }}:3375 consul://{{ inventory_hostname }}:8500'
          restart: unless-stopped
          ports:
            - '3375:3375'
run_once пригодится, чтобы не запустить случайно несколько менеджеров. В кластере действительно может быть несколько менеджеров. Они устраивают выборы, и кто победит, становится primary, а остальные replica. Впрочем, команды можно слать на любой, replica будет пересылать их в primary.
Тут можно указать project_name — ту самую первую часть имени контейнеров, создаваемых Docker Compose, которая обычно берётся из имени каталога, где лежит docker-compose.yml.
Берём образ по имени swarm c Docker Hub, и выполняем в нём команду manage с параметрами.
Ключ -H указывает порт, который будет слушать менеджер. Этот порт нужно пробросить наружу в директиве ports. Для доступа к менеджеру без TLS шифрования принято использовать порт 3375.
Чтобы менеджеры видели друг друга для выборов используется ключ --advertise, где указывается внешнее имя хоста и порт, по которому доступен данный менеджер, так он будет себя объявлять. Ещё нужен ключ --replication без параметров, его я тут не указал, потому что если у вас менеджер один, он рискует ни с кем не договориться, кто здесь главный, и зависнуть в состоянии replica, рой тогда не заведётся.
Последний параметр команды manage — адрес Consul. Менеджеру нужно какое-то распределённое хранилище, чтобы хранить адреса и состояния узлов кластера, а также выбирать главного менеджера. Хранилищем можен быть Consul, или Etcd, или Zookeeper.
Мегаполезный параметр restart у Docker и Docker Compose появился с Докера 1.10. Он говорит демону, нужно ли запускать этот контейнер при старте самого демона. always — запускать всегда. unless-stopped — запускать всегда, если только контейнер не был ранее остановлен явно (через docker stop или docker-compose down). Так как Swarm нам нужен после ребута, ставим этот параметр.
Swarm Manager Backup
Нам нужен кластер Consulа. Конечно же сам Консул будем запускать в докер контейнере. На всех узлах Swarm кластера и там, где запущен Swarm Manager. Чтобы все, кому нужен Consul, могли бы ходить за ним на свой собственный хост. Конечно же для Consul уже есть готовый образ, который нужно запустить с правильными параметрами.
На одном из узлов нужно стартовать сервер Консула.
- name: start consul
  run_once: true
  become: yes
  docker_service:
    project_name: swarm
    definition:
      version: '2'
      services:
        consul:
          image: progrium/consul
          command: "-server -advertise {{ inventory_hostname }} -bootstrap"
          restart: unless-stopped
          ports:
            - '8300:8300'
            - '8301:8301'
            - '8301:8301/udp'
            - '8302:8302'
            - '8302:8302/udp'
            - '8400:8400'
            - '8500:8500'
Сервер должен объявить о себе другим Консулам, для этого нужен ключик -advertise. Ключ -bootstrap говорит, что это сам себе сервер, он начнёт обслуживать запросы, не дожидаясь, пока в сети появятся другие Консулы.
У Консула нужно прокинуть много портов. Какие-то порты используются, чтобы Консулам собраться в свой кластер. А Smarm Manager ходит в Консул по порту 8500 — это REST API для доступа к распределённому key-value хранилищу.
А на других узлах Consul должен войти в существующий кластер. Для этого нужно выдать образу другой набор параметров.
command: "-server -advertise {{ inventory_hostname }} -join {{ manager_host }}"
Вам нужен адрес хоста, где был запущен первый Консул, чтобы передать его здесь в параметр -join.
Consul cluster
Консулы есть, менеджер есть, нужно скомандовать Докерам объединяться. Для этого нужно снова взять образ swarm, только выполнить в нём команду join.
- name: join swarm cluster
  become: yes
  docker_service:
    project_name: swarm
    definition:
      version: '2'
      services:
        node:
          image: swarm
          command: 'join --advertise={{ inventory_hostname }}:2275 consul://{{ inventory_hostname }}:8500'
          restart: unless-stopped
Узел, вступающий в Swarm, должен объявить миру (т.е. менеджеру), на каком хосте и каком порту живёт докер демон. А объявляет он через Консула.
Вступление в Swarm нужно сделать на всех хостах, которые должны войти в кластер и управляться менеджером. Формально менеджер (или кластер менеджеров) не обязан входить в это множество хостов, он может быть сбоку от самого Swarm кластера. Но менеджер работает только когда как-то меняется конфигурация кластера: создаются новые контейнеры. Так что вполне можно разместить его на одном из узлов кластера. Тогда на этом узле будут запущены и swarm manage, и swarm join.
Swarm Join
Почти всё. Теперь можно запускать docker или docker-compose, заставив их подключаться к Swarm Manager. Либо через параметр -H:
docker -H {{ manager_host }}:3375 run ...
Либо через переменную окружения DOCKER_HOST:
DOCKER_HOST=tcp://{{ manager_host }}:3375 docker-compose up
Но есть ограничения. Особенно если вы выполняете эти команды на совершенно левом хосте, который не входит в Swarm. Нельзя билдить контейнеры, т.е. build в docker-compose.yml — запрещён. Вам придётся сборку делать отдельно, а собранные образы выкладывать на Docker Hub или свой Docker Registry.
Внешние volume будут подключены не в файловой системе, где вы запускаете команды, а в файловой системе ноды кластера, где будет запущен контейнер. Значит, если вам нужно подключить какие-то конкретные файлы, вам, вероятно, придётся позаботиться о том, чтобы они оказались на всех нодах кластера, прежде, чем запускать контейнеры. Ansible тут тоже может оказаться весьма полезным.
Линки между контейнерами, когда вы проставляете link между сервисами в одном docker-compose.yml, как ни странно, работают. Однако об этом нужно позаботиться заранее.
Compose, Machine, Swarm
В Swarm добавился новый тип сетейoverlay. Для контейнеров на разных хостах всё прозрачно, они видят друг друга в одной сети. Но для этого Docker Engine должен договориться с другими Docker Engine насчёт проброса трафика. Делает он это опять через Consul, или Etcd, или Zookeeper. Докер демону нужно добавить ещё один параметр запуска, туда, в /etc/default/docker:
--cluster-store=consul://{{ inventory_hostname }}:8500 
Вообще-то рекомендуют для работы overlay сети использовать другой Консул, не тот, что уже используется Swarm Manager. Но и так работает (не для production, как говорится).
Получается Уроборос. Докер демону для работы сети нужен Консул, который запускается в контейнере после старта демона. Но, работает. Видимо потому, что контейнер Консула не использует overlay сеть, Консулы через кучу проброшенных портов друг с другом общаются.
Ну ещё демону нужно заявить о себе. Нужен ещё один параметр:
--cluster-advertise={{ inventory_hostname }}:2275
После добавления параметров и перезапуска демонов, overlay сеть будет работать.
Overlay network
Когда мы деплоим что-нибудь в Swarm, нельзя заранее угадать, на каком именно хосте будет запущен контейнер. По умолчанию он старается запустить равное количество контейнеров (одного типа?) на всех хостах. Но если у вас в docker-compose.yml есть какие-то выделенные сервисы, тот же балансер, который должен висеть на порту по вполне известному адресу конкретного хоста, вам нужно, чтобы контейнер разворачивался на конкретном хосте. Это — можно.
У каждого Docker Engine в Swarm есть метки. Как минимум — имя хоста. Но можно добавлять свои метки. Метки — это строки вида ключ-значение. Ключ считается уникальным, поэтому нельзя навесить несколько меток с одинаковым ключом.
Чтобы добавить метку, нужно передать ещё один параметр запуска многострадальному демону (опять /etc/default/docker). Например, пометим хост с балансером:
--label balancer=yes
Теперь в описание сервиса балансера можно добавить переменную окружения для Swarm:
haproxy:
  image: dockercloud/haproxy
  restart: unless-stopped
  environment:
    'constraint:balancer==yes':
    DOCKER_HOST: 'tcp://{{ manager_host }}:3375'
  links:
    - backend
Осторожно, синтаксис — дикий. constraint:balancer==yes — это имя переменной окружения. А значение у неё — пустое.
Swarm Filters
Ещё раз, через Swarm Manager можно делать почти всё то же, что через обычный Docker Engine. Тот же dockercloud/haproxy, как видите, вполне умеет выдирать из Swarm те контейнеры, которые нужно балансить, только адрес менеджера ему нужно передать через переменную окружения DOCKER_HOST. docker -H {{ manager_host }}:3375 info выдаст подробные сведения о здоровье Swarm кластера: сколько узлов есть, сколько контейнеров на каждом запущено и т.п. docker -H {{ manager_host }}:3375 ps покажет, какие именно контейнеры запущены, на каком хосте именно, и какие порты на каком хосте открыты. DOCKER_HOST=tcp://{{ manager_host }}:3375 docker-compose scale ... создаст нужное количество контейнеров именно во всём кластере, равномерно размазав их по доступным узлам.
Swarm in Production
Порты 2275 и 3375, упомянутые выше, — без шифрования. Но можно настроить TLS шифрование и авторизацию между всеми узлами кластера.
Swarm overview
Ну и напоследок, в Docker Engine v.1.12, который ныне rc1, Swarm будет встроен прямо в Engine. Вместо запуска swarm образов с параметрами, нужно будет запускать команды docker swarm. Я не смотрел, как это работает. Надеюсь, служебных контейнеров понадобится поменьше :)