Docker Swarm. Свой оркестратор

Когда стоит задача по построению среды с требованиями к отказоустойчивости, масштабируемости и возможности распределения нагрузки, то конечно смотришь на системы кластеризации. Если речь идет еще и о контейнерной кластеризации, то выбор в сторону систем оркестрации контейнеров.

Оркестровка — это управление и координация взаимодействия контейнеров.

Docker Swarm является стандартным инструментом кластеризации для Docker. Хосты Docker объединяются в один последовательный кластер, называемый Swarm. Docker Swarm обеспечивает доступность и высокую производительность работы, равномерно распределяя ее по нодам Docker внутри кластера. Это очень похоже на кластеры Kubernetes, а может и наоборот.

Вообще если сравнивать Docker Swarm с Kubernetes, то Kubernetes более масштабен и гибок, но Swarm это инструмент который доступен из коробки и для его разворачивание не нужны такие трудозатраты и вообще он прост, что является его плюсом: рабочий кластер Swarm можно поднять за минуту. Прекрасно подойдет для небольших и средних компаний или проектов, где не нужна гибкость Kubernetes.

В некоторых материал встречаются такие цифры: Docker Swarm можно рассматиривать для запуска не более 50 тысяч контейнеров и 1000 нод. При этих достаточно внушительных цифрах у Swarm-а нет автомасштабирования (autoscaling).

Docker Swarm это по сути состояние Docker Engine работающий в кластере. Плюс кластерные хосты объединяются единым контейнерным пространством.

В режиме Swarm в Docker появляются понятия stack и service:

docker node 
docker stack
docker service

Стеки, сервисы — это еще одни уровни абстракции над контейнерами. У них будут имена, порты, тома и т.д.

Попробуем поднять свой кластер Docker Swarm. Для этого нам необходимо развернуть на какой-либо платформе три хоста (выбор цифры 3 наверно является уже академическим, можно для себя выбрать иное число, главное понять сущность происходящего).

На хостах (нодах) должны быть развернуты Docker Engine.

И так, наш стенд. Я использую Vagrant на Ubuntu 20.04, ноды тоже будут на Ubuntu Focal, на них установлен Docker.

Описание стенда:

Ноды:
master - 192.168.10.11
worker1 - 192.168.10.12
worker2 - 192.168.10.13

Установка Docker Engine на ноды

Для развертывания Docker Engine на Ubuntu используем скрипт:

#!/bin/env bash

sudo apt-get update -y
sudo apt-get install -y \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

echo \
  "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update -y
sudo apt-get install -y docker-ce docker-compose docker-ce-cli containerd.io

Или, что более предпочтительнее для меня, Ansible:

---
- name: 'Install Docker Engine to Ubuntu'
  hosts: all
  become: true

  tasks:
  - name: Install prerequisites for Docker repository
    apt:
      name: ['apt-transport-https', 'ca-certificates', 'curl', 'gnupg2', 'software-properties-common']
      update_cache: yes

  - name: Add Docker GPG key
    apt_key: url=https://download.docker.com/linux/ubuntu/gpg

  - name: Add Docker APT repository
    apt_repository:
      repo: deb [arch=amd64] https://download.docker.com/{{ ansible_system | lower }}/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable

  - name: Install Docker CE
    apt:
      name: ['docker-ce', 'docker-ce-cli', 'containerd.io']
      update_cache: yes

  - name: Install prerequisites for docker-compose
    apt:
      name: ['python3-pip', 'python3-setuptools', 'virtualenv']

  - name: Install docker-compose
    pip:
      name: docker-compose

Инициализация Docker Swarm

Создаем кластер Swarm:

sudo docker swarm init

Если есть несколько интерфейсов на сервере, определяем на каком интерфейсе работает swarm (ip или интерфейс):

sudo docker swarm init --advertise-addr 192.168.10.11 
или
sudo docker swarm init --advertise-addr eth{X}
vagrant@master:~$ sudo docker swarm init --advertise-addr 192.168.10.11
Swarm initialized: current node (c8c1bngf6s7upknjbwykv3q2v) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-0c77d9x5mph6k6zizn93290degg1z2c25rotgbabxrwx4sh0by-f03hbqskhbszllpt4vbb95qvv 192.168.10.11:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

При создании кластера Swarm сгенериет токен для всех участников данного кластера и при подключении к кластеру необходимо указывать данный токен.

Добавляем наши ноды в кластер:

sudo docker swarm join --token SWMTKN-1-0c77d9x5mph6k6zizn93290degg1z2c25rotgbabxrwx4sh0by-f03hbqskhbszllpt4vbb95qvv 192.168.10.11:2377
vagrant@worker1:~$ sudo docker swarm join --token SWMTKN-1-0c77d9x5mph6k6zizn93290degg1z2c25rotgbabxrwx4sh0by-f03hbqskhbszllpt4vbb95qvv 192.168.10.11:2377
This node joined a swarm as a worker.

vagrant@worker2:~$ sudo docker swarm join --token SWMTKN-1-0c77d9x5mph6k6zizn93290degg1z2c25rotgbabxrwx4sh0by-f03hbqskhbszllpt4vbb95qvv 192.168.10.11:2377
This node joined a swarm as a worker.

Проверим состояние нашего кластера:

vagrant@master:~$ sudo docker node ls
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
c8c1bngf6s7upknjbwykv3q2v *   master     Ready     Active         Leader           20.10.5
w0r72yyrxv10fq9zsvdwea7u7     worker1    Ready     Active                          20.10.5
s7klsqwemj419vfbxqg3tvl3l     worker2    Ready     Active                          20.10.5

vagrant@master:~$ sudo docker stack ls
NAME      SERVICES   ORCHESTRATOR

vagrant@master:~$ sudo docker service ls
ID        NAME      MODE      REPLICAS   IMAGE     PORTS

vagrant@master:~$ sudo docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Как видим наш кластер активен, состоит из master, worker1 и worker2. Стеков, сервисов и контейнеров не запущено.

Web интерфейс для Swarm

Не знаю, что лучше, работать через CLI или веб-интерфейс, тут выбор за каждым, но для представления развернем Portainer.

Portainer состоит из двух элементов: сервера Portainer и агента Portainer. Оба элемента работают как легкие контейнеры в кластере Swarm. Агенты обычно разворачиваются на всех нодах кластера Swarm и «информируют» сервер о состоянии нод.

По умолчанию веб-интерфейс Portainer открыт через порт 9000, а агенты через порт 8000.

Кстати, Portainer разворачивают и на Kubernetes кластерах.

Установим:

curl -L https://downloads.portainer.io/portainer-agent-stack.yml -o portainer-agent-stack.yml

docker stack deploy -c portainer-agent-stack.yml portainer

YAML файл для разворачивания стека очень похож на файл для docker-compose:

cat portainer-agent-stack.yml

version: '3.2'

services:
  agent:
    image: portainer/agent
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/lib/docker/volumes:/var/lib/docker/volumes
    networks:
      - agent_network
    deploy:
      mode: global
      placement:
        constraints: [node.platform.os == linux]

  portainer:
    image: portainer/portainer-ce
    command: -H tcp://tasks.agent:9001 --tlsskipverify
    ports:
      - "9000:9000"
      - "8000:8000"
    volumes:
      - portainer_data:/data
    networks:
      - agent_network
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints: [node.role == manager]

networks:
  agent_network:
    driver: overlay
    attachable: true

volumes:
  portainer_data:

Вкратце, из файла видно, что сначала собирается агент с deploy mode: global, т.е. на всех нодах, с условием, что на нодах Linux система, после собирается сервер Portainer:

deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints: [node.role == manager]
  • реплики = 1
  • условие на ноде = manager, т.е. master

Еще раз проверим, что изменилось в нашем кластере:

vagrant@master:~$ sudo docker stack ls
NAME        SERVICES   ORCHESTRATOR
portainer   2          Swarm

vagrant@master:~$ sudo docker service ls
ID             NAME                  MODE         REPLICAS   IMAGE                           PORTS
258z8x88618i   portainer_agent       global       3/3        portainer/agent:latest          
im61j820aio8   portainer_portainer   replicated   1/1        portainer/portainer-ce:latest   *:8000->8000/tcp, *:9000->9000/tcp

Зайдем на веб интерфейс — http://192.168.10.11:9000/

https://documentation.portainer.io/v2.0/deploy/ceinstallswarm/

Разворачиваем свой контейнер в кластере

Для примера возьмём простой контейнер со своей сборкой Nginx и развернем.

sudo docker service create --replicas 2 --publish 8080:80 --name nginx-srv airmeno/alpine-nginx

Проверим наш сервис:

vagrant@master:~$ sudo docker service ls
ID             NAME                  MODE         REPLICAS   IMAGE                           PORTS
zf4m0vk687av   nginx-srv             replicated   2/2        airmeno/alpine-nginx:latest     *:8080->80/tcp
258z8x88618i   portainer_agent       global       3/3        portainer/agent:latest          
im61j820aio8   portainer_portainer   replicated   1/1        portainer/portainer-ce:latest   *:8000->8000/tcp, *:9000->9000/tcp

Тут же можно масштабировать сервис или через cli:

sudo docker service scale zf4m0vk687av=3
vagrant@master:~$ sudo docker service ps zf4m0vk687av
ID             NAME          IMAGE                         NODE      DESIRED STATE   CURRENT STATE            ERROR     PORTS
wf7ypamsd6ij   nginx-srv.1   airmeno/alpine-nginx:latest   worker2   Running         Running 13 minutes ago             
mkv4igumbavk   nginx-srv.2   airmeno/alpine-nginx:latest   worker1   Running         Running 13 minutes ago             
by7wvtw14yq4   nginx-srv.3   airmeno/alpine-nginx:latest   master    Running         Running 3 minutes ago  

Обратное масштабирование сервиса (уменьшаем количество развернутых контейнеров):

sudo docker service scale zf4m0vk687av=1

vagrant@master:~$ sudo docker service ps zf4m0vk687av
ID             NAME          IMAGE                         NODE      DESIRED STATE   CURRENT STATE            ERROR     PORTS
wf7ypamsd6ij   nginx-srv.1   airmeno/alpine-nginx:latest   worker2   Running         Running 34 minutes ago 

Проверим работу контейнера:

Притом не имеет значение по какому из адресов будет обращение к кластеру и работает ли на ноде по этому адресу контейнер.

Разворачиваем стек

Стек — это объединенные по функционалу сервисы. Например у нас есть классическая связка Nginx + PHP-FPM, развернутая на отдельных контейнерах. Создаем сервис для Nginx c определенными параметрами доступности и отказоустойчивости и создаем так же сервис для php-fpm с распределением нагрузки. Для совместного функционирования создается стек. Утрируя, стек это аналог компосера для сервисов.

Команды для стека:

sudo docker stack ls
sudo docker stack ps portainer
sudo docker stack services portainer
sudo docker stack rm portainer

Создаем файл для деплоя стека:

cat my-nginx-stack.yml

version: '3.8'

services:
 nginx:
  image: airmeno/otus-nginx
  ports:
   - 80:80
  volumes:
   - /opt/code:/www:rw
  networks:
   - nginx-php
  deploy:
    mode: replicated
    replicas: 1
    placement:
      constraints: [node.platform.os == linux]
 
 myphp:
  image: airmeno/otus-php
  container_name: myphp
  volumes:
   - /opt/code:/www:rw
  networks:
   - nginx-php
  deploy:
    mode: replicated
    replicas: 2
    placement:
      constraints: [node.platform.os == linux]

networks:
  nginx-php:
sudo docker stack deploy -c my-nginx-stack.yml mynginx 

Проверим:

vagrant@master:~$ sudo docker service ls
ID             NAME                  MODE         REPLICAS   IMAGE                           PORTS
80mf4xnlq5sl   mynginx_myphp         replicated   2/2        airmeno/otus-php:latest         
p65lm40kmz9p   mynginx_nginx         replicated   1/1        airmeno/otus-nginx:latest       *:80->80/tcp
zf4m0vk687av   nginx-srv             replicated   1/1        airmeno/alpine-nginx:latest     *:8080->80/tcp
258z8x88618i   portainer_agent       global       3/3        portainer/agent:latest          
im61j820aio8   portainer_portainer   replicated   1/1        portainer/portainer-ce:latest   *:8000->8000/tcp, *:9000->9000/tc

vagrant@master:~$ sudo docker stack ls
NAME        SERVICES   ORCHESTRATOR
mynginx     2          Swarm
portainer   2          Swarm

Создаем файл по месту назначения из опции volumes /opt/code:/www

sudo bash -c 'echo "<?php phpinfo(); ?>" > /opt/code/index.php'

Проверим наш стек:

Top