Docker swarm или кластер за 15 минут

Все время docker обходил меня стороной, кроме единичных случаев, когда какой-то сервис сложно запустить в нескольких екземплярах на одном хосте. Так было с php, во времена когда docker не был мейнстримом, и с dnscrypt, которому нельзя было указывать несколько dns серверов, и последний раз сталкивался когда необходимо было организовать хитрую фильтрацию писем с изменением содержимого, и пришлось наставить кучу postfix'ов. В остальном же последние пару лет работал с привычной всем виртуализацией в виде proxmox'a с его kvm, и непривычным многим lxc, а до этого с openvz. Но времена меняются :D
Итого что от меня хотели:
  1. Написание роли для docker swarm cluster с балансером
  2. Развернуть любое простое приложение в cluster
  3. Предусмотреть возможность обновления приложения по тэгу из ветки
  4. Предусмотреть обновления Docker swarm cluster, а именно, docker-ce из официального репозитория без нарушения работы приложения.
Роли лежат тут.
Для того чтобы раскатать кластер надо минимум 3 хоста, но у меня их будет 6 - 3 менеджера, 2 воркера и 1 балансер. Первым делом необходимо подготовить виртуалки с ubuntu 16.04, настроеной сетью, установленым python и пользователем, который может выполнять sudo без пароля. Тут на помощь приходит vagrant с предыдущего поста, который поможет локально это все запустить и потестить.
vagrantfile
Vagrant.configure(2) do |config|
  config.vm.box = "ubuntu/xenial64"

  config.vm.define :n1 do |vm_config|
      vm_config.vm.host_name = "n1"
      vm_config.disksize.size = '10GB'
      vm_config.vm.network "private_network", ip:"192.168.35.11"
      vm_config.vm.provider :virtualbox do |vb|
          vb.customize ["modifyvm", :id, "--memory", "2048"]
          vb.customize ["modifyvm", :id, "--cpus", "1"]
          vb.name = 'n1'
      end
  end

  config.vm.define :n2 do |vm_config|
      vm_config.vm.host_name = "n2"
      vm_config.disksize.size = '10GB'
      vm_config.vm.network "private_network", ip:"192.168.35.12"
      vm_config.vm.provider :virtualbox do |vb|
          vb.customize ["modifyvm", :id, "--memory", "2048"]
          vb.customize ["modifyvm", :id, "--cpus", "1"]
          vb.name = 'n2'
      end
  end

  config.vm.define :n3 do |vm_config|
      vm_config.vm.host_name = "n3"
      vm_config.disksize.size = '10GB'
      vm_config.vm.network "private_network", ip:"192.168.35.13"
      vm_config.vm.provider :virtualbox do |vb|
          vb.customize ["modifyvm", :id, "--memory", "2048"]
          vb.customize ["modifyvm", :id, "--cpus", "1"]
          vb.name = 'n3'
      end
  end

  config.vm.define :n4 do |vm_config|
      vm_config.vm.host_name = "n4"
      vm_config.disksize.size = '10GB'
      vm_config.vm.network "private_network", ip:"192.168.35.14"
      vm_config.vm.provider :virtualbox do |vb|
          vb.customize ["modifyvm", :id, "--memory", "2048"]
          vb.customize ["modifyvm", :id, "--cpus", "1"]
          vb.name = 'n4'
      end
  end

  config.vm.define :n5 do |vm_config|
      vm_config.vm.host_name = "n5"
      vm_config.disksize.size = '10GB'
      vm_config.vm.network "private_network", ip:"192.168.35.15"
      vm_config.vm.provider :virtualbox do |vb|
          vb.customize ["modifyvm", :id, "--memory", "2048"]
          vb.customize ["modifyvm", :id, "--cpus", "1"]
          vb.name = 'n5'
      end
  end

  config.vm.define :n6 do |vm_config|
      vm_config.vm.host_name = "n6"
      vm_config.disksize.size = '10GB'
      vm_config.vm.network "private_network", ip:"192.168.35.16"
      vm_config.vm.provider :virtualbox do |vb|
          vb.customize ["modifyvm", :id, "--memory", "2048"]
          vb.customize ["modifyvm", :id, "--cpus", "1"]
          vb.name = 'n6'
      end
  end

  id_rsa_ssh_key_pub = File.read(File.join(Dir.home, ".ssh", "id_rsa.pub"))
  config.vm.provision :shell, :inline => "echo 'Copying local public SSH Key to VM auth_key' && echo '\n' >> /home/ubuntu/.ssh/authorized_keys && echo '#{id_rsa_ssh_key_pub }' >> /home/ubuntu/.ssh/authorized_keys && chmod 600 /home/ubuntu/.ssh/authorized_keys"
  config.vm.provision :shell, :inline => "apt-get update -y && apt-get install aptitude -y && aptitude safe-upgrade -y &&  aptitude install net-tools bind-utils mc htop iftop python -y"

  # need for git clone repo
  config.ssh.forward_agent = "true"

end
Делаем vagrant up не забыв сгенерить ключ ssh, ну или он должен уже быть. В файлике можно посмотреть откуда и куда оно что закидывает, и получаем 6 виртуалок, пригодных для экспериментов. У меня на i5-7500 и ssd это занимает около 7 минут.
Запускаем...
ansible-playbook create-swarm.yml -u ubuntu -i conf.d/hosts -s
Тут просто ставится docker-ce определенной версии и второй шаг непосредственно создание кластера. Если вкратце, сначала выполняется инициализация лидера, регистрируются токены для рабочих/управляющих, далее добавляются менеджеры и рабочие ноды, они же воркеры. Менеджеры они так же являюся воркером и могут запускать сервисы.
Что касается выкатки сервисов и обновлений, есть нормальная дока, выглядит как магия
У меня это так: выкатываем сервис - nginx версии 1.9.2 с количеством реплик 4 и тут же у нас выкатывается балансер на основе haproxy
ansible-playbook docker-service.yml -u ubuntu -i conf.d/hosts -s 
Если все прокатилось, можно стрельнуть в проксю, и если все ок, должен быть ответ.
wget -O - 192.168.35.16
--2018-06-07 23:33:38--  http://192.168.35.16/
Connecting to 192.168.35.16:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 612 [text/html]
Saving to: ‘STDOUT’
...
Суть обновлений без downtime основана на репликах, а именно --replicas 4, минимум надо 2. Пока один обновляется, второй принимает запросы. Количество реплик можно менять через scale, например:
docker service scale nginx=6
Достаточно изменить количество реплик в плейбуке и версию, которую можно взять тут:
wget -q https://registry.hub.docker.com/v1/repositories/nginx/tags -O -  | sed -e 's/[][]//g' -e 's/"//g' -e 's/ //g' | tr '}' '\n'  | awk -F: '{print $3}'
И запустить заново, по окончанию у нас будет другая версия и измененное количество реплик
ansible-playbook docker-service.yml -u ubuntu -i conf.d/hosts -s
Что касается обновления docker-ce, у нас же кластер и в этом его плюс, можно одну ноду выкинуть, уронить и тп. Сервисы запустятся на других доступных, тушим одну, обновляем, запускаем, тушим вторую и тп. Для этого плейбук upgrade-docker-ce.yml с манагерами проблем нет, однако с воркерами возникли сложности. Решил через выход из кластера, обновлении и запихивании его обратно. Не уверен на счет правильности данного метода. В противном случае можно раскидать ключи и ходить на управляющие ноды через delegate_to.
Обновляем так
ansible-playbook upgrade-docker-ce.yml -u ubuntu -i conf.d/hosts -s
Cсылки:
Docker swarm mode (режим роя)
Краткое введение в Docker Swarm mode