Docker Swarm: stack deploy и именованные тома (named volumes)

При переходе на 3.х версию файла docker-compose.yml (необходимое требование для работы Docker Swarm) пропадает возможность использовать Data-only контейнеры — специальные контейнеры-спутники, файловая система которых служит для хранения данных и подключается к остальным сервисам с помощью параметра volumes-from.

Давайте разберемся с возможным решением данной проблемы на реальном примере!

В примере используются сервисы [docker-контейнеры] nginx (в качестве фронтенда) и php-fpm (бэкенд), которым необходим доступ к одному и тому же каталогу с кодом. Используя docker-compose.yml второй версии, мы вправе были написать так:

version: '2'
services:
### Applications Code Container #############################
    applications:
        container_name: application
        image: registry.gitlab.lc:5000/develop/ed/develop.sources:latest
### PHP-FPM Container #######################################
    php-fpm:
        container_name: php-fpm
        image: registry.gitlab.lc:5000/develop/ed/php-fpm-ed-sq:latest
        volumes_from:
            - applications
        expose:
            - "9000"
### Nginx Server Container ##################################
    nginx:
        container_name: nginx
        image: registry.gitlab.lc:5000/develop/ed/nginx-ed-sq:latest
        volumes_from:
            - applications
        depends_on:
            - "php-fpm"
        ports:
            - "80:80"
            - "443:443"

Dockerfile для сборки data-only контейнера (application) в процессе CI выглядит так (BRANCH_NAME — имя git-ветки, для которой выполняется CI):

FROM registry.gitlab.lc:5000/develop/ed/image_for_sources:latest
ARG BRANCH_NAME=
COPY ./bin /var/www/${BRANCH_NAME}/bin
COPY ./build /var/www/${BRANCH_NAME}/build
COPY ./config /var/www/${BRANCH_NAME}/config
COPY ./data /var/www/${BRANCH_NAME}/data
COPY ./.env /var/www/${BRANCH_NAME}/
COPY ./grunt /var/www/${BRANCH_NAME}/grunt
COPY ./init_autoloader.php /var/www/${BRANCH_NAME}/
COPY ./library /var/www/${BRANCH_NAME}/library
COPY ./module /var/www/${BRANCH_NAME}/module
COPY ./postcss.config.js /var/www/${BRANCH_NAME}/
COPY ./public /var/www/${BRANCH_NAME}/public
COPY ./vendor /var/www/${BRANCH_NAME}/vendor
COPY ./zf /var/www/${BRANCH_NAME}/
VOLUME /var/www/${BRANCH_NAME}

При изменении data-only контейнера для обновления содержимого каталога /var/www/${BRANCH_NAME} в контейнерах nginx и php-fpm необходимо выполнить команды:

docker-compose down && docker compose up -d

которые неизменно повлекут за собой простой (downtime) ~10-15 секунд (если контейнеров больше, чем в данном примере, то время простоя также возрастает).

Первым этапом на пути к Docker Swarm стало изменение файла docker-compose.yml (перевод его на третью версию с использованием именованных томов):

version: '3'
services:
### Code from branch develop #################################
  applications:
    container_name: application
    image: registry.gitlab.lc:5000/develop/ed/develop.sources:latest
    volumes:
      - developcode:/var/www/develop
### PHP-FPM ##################################################
  php-fpm:
    container_name: php-fpm
    image: registry.gitlab.lc:5000/develop/ed/php-fpm-ed-sq:latest
    volumes:
      - developcode:/var/www/develop
    expose:
      - "9000"
### Nginx ####################################################
  nginx:
    container_name: nginx
    image: registry.gitlab.lc:5000/develop/ed/nginx-ed-sq:latest
    volumes:
      - developcode:/var/www/develop
    ports:
      - "80:80"
      - "443:443"
### Volumes Setup ############################################
volumes:
  developcode:

Однако в этом случае возникли проблемы с обновлением содержимого каталога /var/www/develop в контейнерах nginx и php-fpm. Дело в том, что при первом выполнении команды docker-compose up -d создается пустой именованный том developcode, и, так как он пустой, в него копируется содержимое каталога /var/www/develop из контейнера application. При повторном запуске docker-compose up -d именованный том уже существует, и он не пустой (содержит код предыдущего «релиза»), следовательно содержимое тома developcode будет скопировано в каталоги /var/www/develop контейнеров application, nginx и php-fpm.

Решение данной проблемы находится быстро, но добавляет еще ~5-10 секунд к времени простоя: нужно выполнять команду docker-compose down с дополнительным ключом --volumes или -v. Таким образом, именованные тома будут удалены, а при следующем вызове docker-compose up -d вновь создастся пустой том, в который скопируются файлы из data-only контейнера.

Так как наща цель избавиться от времени простоя, то нужно использовать для развертывания/обновления стека сервисов команду docker stack deploy. Однако здесь мы также сталкиваемся с той же проблемой — содержимое каталога /var/www/develop в контейнерах nginx и php-fpm не обновляется. Использование связки docker stack rm ... && docker stack deploy ... нам не подходит — во-первых, это не исключает даунтайма, во-вторых именованные тома не удаляются (и дополнительных ключей для этого нет), а значит код не будет обновлен.

После множества вопросов заданных с docker-сообществах и переписках с признанными docker-капитанами (Elton Stoneman и Bret Fisher) мне предложили отказаться от использования томов, переделать процесс CI и сразу добавлять код в оба контейнера nginx и php-fpm — мол, это лучшие практики использования docker.

Я уже почти согласился с данным предложением (хотя мне откровенно не нравилось добавлять 337MB «обвески» контейнерам nginx и php-fpm и пересобирать их по 15-25 раз в день) но решил попробовать реализовать еще один «велосипед» с использованием переменных в именах томов.

Первая же попытка в таком виде:

version: '3'
services:
### Code from branch develop #################################
  applications:
    image: registry.gitlab.lc:5000/develop/ed/develop.sources:latest
    volumes:
      - ${VOLUME_NAME}:/var/www/develop
### PHP-FPM ##################################################
  php-fpm:
    image: registry.gitlab.lc:5000/develop/ed/php-fpm-ed-sq:latest
    volumes:
      - ${VOLUME_NAME}:/var/www/develop
### Nginx ####################################################
  nginx:
    image: registry.gitlab.lc:5000/develop/ed/nginx-ed-sq:latest
    volumes:
      - ${VOLUME_NAME}:/var/www/develop
    ports:
      - "80:80"
      - "443:443"
### Volumes Setup ############################################
volumes:
  ${VOLUME_NAME}:

привела к неудаче:

...
volumes value Additional properties are not allowed ('${VOLUME_NAME}' was unexpected)
...

Как оказалось, это нормальное поведение — docker-compose умеет работать в переменными в значениях, но не в ключах (подробности). И вообще, как оказалось использование переменных в режиме роя (Swarm) с командой stack deploy это целая эпопея.

Но ведь никто нам не запрещает использовать external тома! Теперь наш docker-compose.yml полностью готов к развертыванию через stack deploy и выглядит так:

version: '3.1'
services:
### Code from branch develop #################################
  applications:
    image: registry.gitlab.lc:5000/develop/ed/develop.sources:latest
    volumes:
      - developcode:/var/www/develop
    deploy:
      replicas: 1
      update_config:
        parallelism: 1
        delay: 5s
      restart_policy:
        condition: on-failure
      placement:
        constraints: [node.role == manager]
### PHP-FPM ##################################################
  php-fpm:
    image: registry.gitlab.lc:5000/develop/ed/php-fpm-ed-sq:latest
    volumes:
      - developcode:/var/www/develop
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 5s
      restart_policy:
        condition: on-failure
      placement:
        constraints: [node.role == manager]
### Nginx ####################################################
  nginx:
    image: registry.gitlab.lc:5000/develop/ed/nginx-ed-sq:staging
    volumes:
      - developcode:/var/www/develop
    ports:
      - "80:80"
      - "443:443"
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 5s
      restart_policy:
        condition: on-failure
      placement:
        constraints: [node.role == manager]
### Volumes Setup ############################################
volumes:
  developcode:
    external:
      name: code-${VER}

Процесс деплоя сервисов усложнился на одну команду (создание именованного тома с определенным именем) и выглядит так:

export VER=1.1 && docker volume create --name code-$VER
docker stack deploy --with-registry-auth --compose-file docker-compose.yml MY_STACK

Стоит отметить, что данное решение пока не тестировалось на продакшене (да и вообще, кластер Docker Swarm в данном примере состоял из одной ноды) — это просто демонстрация возможности использования data-only контейнеров.