В одном из прошлых постов про проверку состояния контейнеров в Docker ближе к концу поста мне удалось запустить Swarm сервис из локально собранного образа. Ну как удалось.. Собрал и запустил. Но что меня удивило, Docker в Swarm режиме мне это позволил. Всё-таки в нём могло быть больше, чем один хост, а образ я создавал только на первом. Что если бы Swarm запустил сервис на ноде, где образа не было? Или отмасштабировал? Он же не стал бы автоматически копировать образы между хостами, так ведь? Или всё-таки стал бы?
Попробуем-ка сегодня мы отмасштабировать сервис на базе кастомного образа и посмотрим, получится ли. Спойлер: без локального реестра образов получится так себе.
Попытка 1. Просто используем локально собранный образ
Создаём Swarm
Как обычно, на моей машине установлен docker-machine
и VirtualBox, поэтому создавать новый кластер можно с закрытыми глазами и правой рукой, привязанной к креслу. Во-первых, создадим хосты для Swarm менеджера и его работяг:
1 2 3 4 5 6 7 8 9 |
$ docker-machine create master $ docker-machine create worker-1 $ docker-machine create worker-2 $ docker-machine ls # NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS # master - virtualbox Running tcp://192.168.99.100:2376 v17.06.0-ce # worker-1 - virtualbox Running tcp://192.168.99.101:2376 v17.06.0-ce # worker-2 - virtualbox Running tcp://192.168.99.102:2376 v17.06.0-ce |
Легкотня. Затем инициализируем кластер на мастере через команду docker swarm init
и запускаем произведённую им ‘join’ команду на остальных хостах:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
docker-machine ssh master \ docker swarm init --advertise-addr 192.168.99.100 # Swarm initialized: current node (uiywvzl6p0guvbxgrhv1td7jz) is now a manager. # # To add a worker to this swarm, run the following command: # # docker swarm join --token SWMTKN-1-2ok9r6uroyreyghfpkqlj42cninm63rzy4vifjiu9rzg8z0okv-7lu70r5goheti58bcr74m00zf 192.168.99.100:2377 # # To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. docker-machine worker-1 \ ssh docker swarm join --token SWMTKN-1-2ok9r6uroyreyghfpkqlj42cninm63rzy4vifjiu9rzg8z0okv-7lu70r5goheti58bcr74m00zf 192.168.99.100:2377 # This node joined a swarm as a worker. docker-machine ssh worker-2 \ docker swarm join --token SWMTKN-1-2ok9r6uroyreyghfpkqlj42cninm63rzy4vifjiu9rzg8z0okv-7lu70r5goheti58bcr74m00zf 192.168.99.100:2377 # This node joined a swarm as a worker. |
Кластер готов. Сейчас мы быстренько добавим ему в помощники сервис для визуализации контейнеров и приступим, собственно, к делу.
Устанавливаем сервис визуализации
Когда имеешь дело с несколькими хостами и контейнерами, визуальный фидбэк может оказаться очень даже полезным, так что не будет лишним установить его прямо сейчас. Это достаточно просто сделать: подключаем локальный Docker клиент к Docker Engine master
хоста, и запускаем сервис.
1 2 3 4 5 6 7 8 9 10 |
# Connect to Docker Engine in master eval $(docker-machine env master) # Deploy visualization service docker service create \ --name=viz \ --publish=8080:8080 \ --constraint=node.role==manager \ --mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \ dockersamples/visualizer |
Проверяем порт 8080 на master
‘е, и таки да, там всё красочно и прекрасно. Можно переходить к основной части нашего балета.
Создаём кастомный образ для опытов
Хотя абсолютно любой Dockerfile нам бы подошёл, я всё-таки решил скопипастить уже готовый из поста про проверки состояния контейнеров. Всё-таки он уже один раз отработал, так что хоть какой-то неопределённости будет меньше. Сам Dockerfile:
1 2 3 4 5 |
FROM node COPY server.js / EXPOSE 8080 8081 HEALTHCHECK --interval=5s --timeout=10s --retries=3 CMD curl -sS 127.0.0.1:8080 || exit 1 CMD [ "node", "/server.js" ] |
И его товарищ — server.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
"use strict"; const http = require('http'); function createServer () { return http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('OK\n'); }).listen(8080); } let server = createServer(); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); if (server) { server.close(); server = null; res.end('Shutting down...\n'); } else { server = createServer(); res.end('Starting up...\n'); } }).listen(8081); |
Так как наш локальный докер клиент всё ещё подключён к master
, можно просто запустить команду сборки образа и она очутится прямо в Swarm. По крайней мере на одной его машине.
1 2 3 |
docker build . -t server:latest # ... # Successfully tagged server:latest |
А теперь основная часть: делаем сервис и масштабируем его по кластеру.
Запускаем и масштабиуем сервис
Запустить сервис из кастомного образа на машине Swarm, на которой он к тому же и собирался, — просто. Это уже получилось однажды, получится и сейчас.
1 2 3 4 5 6 7 |
docker service create --name=node-server server # image server:latest could not be accessed on a registry to record # its digest. Each node will access server:latest independently, # possibly leading to different nodes running different # versions of the image. # # be04kf5lkimmwganpqgii1j7h |
Кстати, в прошлый раз я не вчитывался, но вывод из service create
действительно предупредил, что образ лежит только на локальной машине, поэтому в любом другом месте кластера может случиться неожиданность. Я уже начинаю догадываться, чем закончится эксперимент.
Сервис визуализации подтверждает, что команда всё-таки сработала:
Ну что, момент истины: отмасштабируется ли он?
1 2 |
docker service scale node-server=3 # node-server scaled to 3 |
Бздыщ! Отмасштабировался. Но как? Проверим-ка, что нам скажут таски сервиса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
docker service ls # ID NAME MODE REPLICAS IMAGE PORTS # be04kf5lkimm node-server replicated 3/3 server:latest # fbpsri2f35zd viz replicated 1/1 dockersamples/visualizer:latest *:8080->8080/tcp docker service ps be04kf5lkimm # ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS # m7rk2hj6uaoq node-server.1 server:latest master Running Running 8 seconds ago # kgq0oto5d1o8 \_ node-server.1 server:latest worker-2 Shutdown Rejected 21 seconds ago "No such image: server:latest" # tb1d1q8rarl4 \_ node-server.1 server:latest worker-2 Shutdown Rejected 27 seconds ago "No such image: server:latest" # vgm1k2spyhnj \_ node-server.1 server:latest worker-2 Shutdown Rejected 31 seconds ago "No such image: server:latest" # njsqyfu0jt1m \_ node-server.1 server:latest worker-1 Shutdown Rejected 36 seconds ago "No such image: server:latest" # u0yj5jteuali node-server.2 server:latest master Running Running 9 seconds ago # w6h0m9s02hbd \_ node-server.2 server:latest worker-2 Shutdown Rejected 23 seconds ago "No such image: server:latest" # 3go9gqapkt3g \_ node-server.2 server:latest worker-1 Shutdown Rejected 29 seconds ago "No such image: server:latest" # j6yzc4k8lblm \_ node-server.2 server:latest worker-1 Shutdown Rejected 33 seconds ago "No such image: server:latest" # 9679vv7vjis3 \_ node-server.2 server:latest worker-2 Shutdown Rejected 35 seconds ago "No such image: server:latest" # j33pwzps37k8 node-server.3 server:latest master Running Running 29 seconds ago |
Ха! Он положил все реплики сервиса на master
хост — единственное место, где лежал кастомный образ. Что прикольно, в списке задач много тех, которые пытались положить контейнер на worker
хосты, но упали с ошибкой No such image: server:latest
. Нет на них образа и всё тут. Так что предположение, что Swarm может копировать образы между хостами — не подтвердилось. Будем пробовать что-то ещё.
Удаляем наш сервис и двигаемся к следующей попытке.
1 |
docker service rm node-server |
Попытка 2. Используем локальный Docker реестр образов
Вполне очевидный альтернативный подход — использовать какое-то подобие Docker Hub, но внутри Swarm — локальный реестр. Если он будет доступен внутри кластера, то наш кастомный образ можно запушить (docker push
) в него, и создавать сервисы со ссылкой на внутренний реестр.
Вообще, создание собственного реестра вполне тривиально. Это просто запуск контейнера, вроде docker run -d -p5000:5000 registry:latest
. Но есть загвоздка: HTTP-реестры доступны только по localhost, так что если мы хотим, чтобы к нему могли достучаться все хосты кластера, нужно использовать HTTPS и все связанные SSL сертификатами замуты (Update: на самом деле нет. Как подсказали умные люди, localhost тоже шарится внутри кластерных машин, так что если мы не собираемся пускать в реестр людей и роботов извне, то можно остановиться и на HTTP. Я до сих пор в шоке — для меня localhost всегда был чем-то сугубо интимным для каждого конкретного хоста). Пользоваться
insecure-registries настройкой в Docker Engine мне сегодня не хочется, так что будем колдовать с самописными SSL сертификатами.
В общем, план такой:
- Создаём SSL сертификат для адреса, например, myregistry.com, за которым будет скрываться наш реестр.
- Убеждаем Docker доверять этому сертификату.
- Добавляем myregistry.com в
/etc/hosts
наmaster
иworker-*
ноды, чтобы они знали, о чём идёт речь. - Создаём реестр в HTTPS режиме.
- Пушим наш
server:latest
образ в реестр. - Создаём сервис из образа в локальном реестре.
Шагов много, так что начнём с первого.
Создаём SSL сертификат
К счастью, на никсовых системах это просто:
1 2 3 |
openssl req -newkey rsa:4096 -nodes -sha256 \ -keyout registry.key -x509 -days 365 \ -out registry.crt |
На все вопросы, которые сертификат спрашивал по ходу дела, я отвечал пустой строкой. Ну, кроме Common Name — тот важен. В него я ввёл адрес реестра: myregistry.com.
Убеждаем Docker верить сертификату
Это тоже был бы простой шаг, но когда я пишу (или перевожу) посты в час ночи, наиболее простые решения часто проходят мимо, а остаются такие, как это. На каждом хосте кластера мне потребовалось три шага, чтобы положить сертификат в хранилище Docker Engine и тем самым убедить его в нашей благонадёжности:
- Копируем registry.crt файл на Swarm хост,
- создаём папку для сертификата в хранилище,
- ложим в неё registry.crt.
1 2 3 |
docker-machine scp registry.crt master:/home/docker/ && \ docker-machine ssh master sudo mkdir -p /etc/docker/certs.d/myregistry.com:5000 && \ docker-machine ssh master sudo mv /home/docker/registry.crt /etc/docker/certs.d/myregistry.com:5000/ca.crt |
После этих трёх шагов master
нам точно станет доверять, но ещё остаётся worker-1
и worker-2
, так что придётся повторить. Дважды.
Добавляем myregistry.com в /etc/hosts
Реестр будет на master
ноде, его айпишка — 192.168.99.100
, так что она и пойдёт в /etc/hosts кластерных машин. Например, для мастера это будет выглядеть вот так:
1 2 3 |
docker-machine ssh master sudo sh -c "echo ' 192.168.99.100 myregistry.com' >> /etc/hosts" exit |
Создаём HTTPS реестр
Реестр сервису потребуется доступ к файлам сертификата, так что их нужно будет скопировать в master
. Можно было бы конечно использовать docker secret
вместо этого, но ай.
1 2 |
docker-machine scp registry.crt master:/home/docker/ && \ docker-machine scp registry.key master:/home/docker/ |
А теперь, собственно, реестр:
1 2 3 4 5 6 7 |
docker service create --name registry --publish=5000:5000 \ --constraint=node.role==manager \ --mount=type=bind,src=/home/docker,dst=/certs \ -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 \ -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.crt \ -e REGISTRY_HTTP_TLS_KEY=/certs/registry.key \ registry:latest |
Вот так-то. У нас появился ещё один цветной квадратик на кластерной карте:
Пушим образ в локальный реестр
Можно было бы привязать уже существующий server:latest
образ в нашему реестру, но это примерно такой же объём нажатий клавиш, что и для создания нового образа, так что лучше соберём новый:
1 |
docker build . -t myregistry.com:5000/server:latest |
Тэг образа начинается с адреса реестра, так что Docker сразу догадается, куда его нужно отправить:
1 2 3 |
docker push myregistry.com:5000/server:latest # The push refers to a repository [myregistry.com:5000/server] # 309eab97be6f: Pushed |
Теоретически, наш образ теперь прячется в Swarm реестре. Сейчас мы это проверим.
Создаём и масштабируем сервис
Момент истины:
1 2 |
docker service create --name=node-server myregistry.com:5000/server docker service scale node-server=3 |
Карта кластера:
Муа-ха-ха! Писание не соврало, и из локального реестра worker машины смогли скачать себе образ без проблем. Подход работает.
Мораль
Использовать кастомные Docker образы в Swarm чуть сложнее, чем в одиночном Docker. Если только мы не раскидали свой образ по всем хостам облака, что даже звучит сложно, с запуском и масштабированием сервиса на его основе будут сюрпризы. Проблема исчезает, если использовать свой собственный локальный реестр. Если не бояться SSL, то разворачивается он относительно просто даже с самописными сертификатами.
Если же образы будут создаваться и использоваться только внутри кластера, то можно даже не заморачиваться SSL и использовать простой HTTP реестр и обращаться к нему по localhost со всех нодов кластера. Это тоже работает.