Наш опыт знакомства с Docker

    Вместо предисловия





    Сегодня приснился сон, как-будто меня ужали до размера нескольких
    килобайт, засунули в какой-то сокет и запустили в контейнере.
    Выделили транспорт в оверлейной сети и пустили
    тестировать сервисы в других контейнерах…
    Пока не сделали docker rm


    Не так давно мне посчастливилось стать членом очень крутой команды
    Centos-admin.ru, в которой я познакомился с такими же, как я: единомышленниками со страстью к новым технологиям, энтузиастами и просто отличными парнями. И вот, уже на второй рабочий день меня с коллегой посадили работать над одним проектом, в котором требовалось «докерировать всё, что можно докеризировать» и было критически важно обеспечить высокую доступность сервисов.

    Скажу сразу, что до этого я был обычным комнатным Linux-админом: мерился аптаймами, апт-гет-инсталлил пакеты, правил конфиги, перезапускал сервисы, тайлил логи. В общем, не имел особо выдающихся практических навыков, совершенно ничего не знал о концепции The Pets vs. Cattle, практически не был знаком с Docker и вообще очень слабо представлял, какие широкие возможности он скрывает. А из инструментов автоматизации использовал лишь ansible для настройки серверов и различные bash-скрипты.



    Исходя из опыта, который нам удалось получить при работе с этим проектом, хотелось бы немного им поделиться.



    Какие задачи должен был решать наш докеризированный кластер:


    — динамическая инфраструктура.
    — быстрое внедрение изменений.
    — упрощение разворачивания приложений.

    Инструменты, которые использовались:


    — Docker
    — Docker swarm (agent + manage)
    — Consul
    — Registrator
    — Consul Template
    — Docker compose
    — руки

    Описание инструментов:



    Docker





    О Docker уже было немало статей, в том числе и на хабре. Я думаю не стоит в подробностях описывать, что это такое.
    Инструмент, который упрощает жизнь всем. Разработчику, тестировщику, сисадмину, архитектору.
    Docker позволяет нам создавать, запускать, деплоить практически любые приложения и практически на любой платформе.
    Docker можно сравнивать с git, но не в контексте работы с кодом, а в контексте работы с приложением в целом.

    Здесь можно долго рассказывать о прелестях этого замечательного продукта.

    Docker swarm





    Swarm предоставляет функционал логического объединения всех наших хостов (node) в один кластер.
    Он работает таким образом, что нам не придется думать о том, на какой ноде необходимо запустить тот или иной контейнер. Swarm это делает за нас. Мы лишь хотим запустить приложение «где-то там».
    Работая со Swarm — мы работаем с пулом контейнеров. Swarm использует Docker API для работы с контейнерами.

    Обычно, при работе в командной строке, бывает удобно указать переменную
    export DOCKER_HOST=tcp://<my_swarm_ip>:3375

    и использовать команды docker как обычно, но уже работая не с локальной нодой, а с кластером в целом.

    Обратите внимание на параметр --label. С помощью него мы можем указывать ноде метки. К примеру, если у нас есть машина с SSD-дисками и нам необходимо запустить контейнер с PosrgreSQL уже не «где-то там», в кластере, а на той ноде, в которой установлены быстрые диски.

    Назначаем демону ноды метку:
    docker daemon --label com.example.storage="ssd"

    Запускаем PostgreSQL с фильтром у указанной метке:
    docker run -d -e constraint:com.example.storage="ssd" postgres


    Подробнее о фильтрах

    Стоит также рассмотреть такой параметр как startegy в кластере Swarm. Этот параметр позволяет более эффективно распределять нагрузку между нодами кластера.
    Ноде можно назначить три параметра strategy:

    — spread
    Используется по-умолчанию, если не указан другой параметр strategy. В этом случае, swarm будет запускать новый контейнер, если на этой ноде запущено меньшее количество контейнеров, чем на других нодах. Данный параметр не учитывает состояние контейнеров. Они все даже могут быть остановлены, но эта нода не будет выбрана для запуска нового контейнера на ней.

    — binpack
    С этим параметром, наоборот, swarm постарается забить каждую ноду контейнерами под завязку. Здесь также учитываются остановленные контейнеры.

    — random
    Название говорит само за себя.

    Consul





    Consul — это очередной замечательный продукт от банды Митчелла Хашимото, компании Hashicorp, которая радует нас такими замечательными инструментами как Vagrant и многими другими.
    Consul выполняет роль распределенного консистентного хранилища конфигураций, которое поддерживается в актуальном состоянии registrator'ом.
    Состоит из агентов и серверов (кворум серверов N/2+1). Агенты запускаются на нодах кластера и занимаются регистрацией сервисов, выполнением сценариев проверки и сообщает о результатах Consul-server.
    Также имеется возможность использовать Consul как key-value хранилище, для более гибкого конфигурирования связей контейнеров.
    Помимо этого Consul функционирует как health-checker по имеющемуся у него списку проверок, который так же поддерживает в нем Registrator.
    Имеется web-UI, в котором можно просматривать состояние сервисов, проверок, нод, ну и, конечно же, REST API.

    Немного о проверках:

    Script

    Проверка скриптом. Скрипт должен возвращать статус код:

    — Exit code 0 — проверка в статусе passing (т. е. с сервисом всё хорошо)
    — Exit code 1 — Проверка в статусе warning
    — Any other code — Проверка в статусе failing

    Пример:
    #!/usr/bin/with-contenv sh
    
    RESULT=`redis-cli ping`
    
    if [ "$RESULT" = "PONG" ]; then
        exit 0
    fi
    
    exit 2
    
    


    В документации также приводятся примеры использования чего-то похожего на nagios-плагины
    {
      "check": {
        "id": "mem-util",
        "name": "Memory utilization",
        "script": "/usr/local/bin/check_mem.py",
        "interval": "10s"
      }
    }
    
    


    gist.github.com/mtchavez/e367db8b69aeba363d21

    TCP

    Стучится на сокет указанного хостнейма/IP-адреса. Пример:
    {
        "id": "ssh",
        "name": "SSH TCP on port 22",
        "tcp": "127.0.0.1:22",
        "interval": "10s",
        "timeout": "1s"
    }
    


    HTTP

    Пример стандартной HTTP-проверки:

    Помимо регистрации проверок через REST API Consul, проверки можно навешивать при запуске контейнера с помощью аргумента -l (label)
    Для примера я запущу контейнер с django+uwsgi внутри:
    docker run -p 8088:3000 -d --name uwsgi-worker --link consul:consul -l "SERVICE_NAME=uwsgi-worker" -l "SERVICE_TAGS=django" \
    -l "SERVICE_3000_CHECK_HTTP=/" -l "SERVICE_3000_CHECK_INTERVAL=15s" -l "SERVICE_3000_CHECK_TIMEOUT=1s" uwsgi-worker


    В UI Консула увидим заголовок стандартной страницы django. Видим, что статус проверки — passing, значит, с сервисом всё в порядке.



    Или можно сделать запрос к REST API по http:
    curl http://<consul_ip>:8500/v1/health/service/uwsgi-worker | jq .

    [
      {
        "Node": {
          "Node": "docker0",
          "Address": "127.0.0.1",
          "CreateIndex": 370,
          "ModifyIndex": 159636
        },
        "Service": {
          "ID": "docker0:uwsgi-worker:3000",
          "Service": "uwsgi-worker",
          "Tags": [
            "django"
          ],
          "Address": "127.0.0.1",
          "Port": 8088,
          "EnableTagOverride": false,
          "CreateIndex": 159631,
          "ModifyIndex": 159636
        },
        "Checks": [
          {
            "Node": "docker0",
            "CheckID": "serfHealth",
            "Name": "Serf Health Status",
            "Status": "passing",
            "Notes": "",
            "Output": "Agent alive and reachable",
            "ServiceID": "",
            "ServiceName": "",
            "CreateIndex": 370,
            "ModifyIndex": 370
          },
          {
            "Node": "docker0",
            "CheckID": "service:docker1:uwsgi-worker:3000",
            "Name": "Service 'uwsgi-worker' check",
            "Status": "passing",
            "Notes": "",
            "Output": "",
            "ServiceID": "docker0:uwsgi-worker:3000",
            "ServiceName": "uwsgi-worker",
            "CreateIndex": 159631,
            "ModifyIndex": 159636
          }
        ]
      }
    ]
    
    


    Пока сервис по HTTP отдаёт статус ответа 2xx, Consul считает его живым и здоровым. Если код ответа 429 (Too Many Request) — проверка будет в состоянии Warning, все остальные коды будут отмечаться как Failed и Consul пометит этот сервис как failure.
    По-умолчанию интервал http-проверки — 10 секунд. Можно задать другой интервал, определив параметр timeout.
    Consul Template, в свою очередь, основываясь на результате проверки, генерирует конфигурационный файл балансировщику, с N-ным количеством «здоровых» воркеров и балансировщик отправляет запросы к воркерам.

    Регистрация новой проверки в консуле:
    curl -XPUT -d @_ssh_check.json http://<consul_ip>:8500/v1/agent/check/register


    Где в файле ssh_check.json указываются параметры проверки:
    {
        "id": "ssh",
        "name": "SSH TCP on port 22",
        "tcp": "<your_ip>:22",
        "interval": "10s",
        "timeout": "1s"
    }
    
    


    Отключение проверки:
    curl http://<consul_ip>:8500/v1/agent/check/deregister/ssh_check


    Возможности Consul очень велики и, к сожалению, охватить их все в рамках одной статьи проблематично.
    Желающие могут обратиться к официальной документации в которой очень много примеров и достаточно хорошо обо всём расписано.

    Registrator



    Registrator выполняет роль информатора об изменениях запущенных контейнеров Docker. Он мониторит списки контейнеров и вносит соответствующие правки в Consul в случае старта или остановки контейнеров. В том числе и создание новых контейнеров Registrator немедленно отражает в списке сервисов в Consul.
    Так же он добавляет записи для health-check в Consul, на основе метаданных контейнеров.
    Например, при запуске контейнера командой:
    docker run --restart=unless-stopped -v /root/html:/usr/share/nginx/html:ro --links consul:consul -l "SERVICE_NAME=nginx" -l "SERVICE_TAGS=web"     -l "SERVICE_CHECK_HTTP=/" -l "SERVICE_CHECK_INTERVAL=15s" -l "SERVICE_CHECK_TIMEOUT=1s" 
     -p 8080:80 -d nginx


    Registrator добавит в Consul сервис nignx и создаст HTTP-проверку для этого сервиса.

    Подробнее

    Consul Template



    Очередной замечательный инструмент от ребят из Hashicorp. Он обращается к Consul и в зависимости от состояния параметров/значений, находящихся в нём, может генерировать содержимое файлов по своим шаблонам, например, внутри контейнера. Consul Template при обновлении данных в Consul также может выполнять различные команды.
    Пример:

    NGINX:

    Создадим файл server.conf.ctmpl
    upstream fpm {
            least_conn;
            {{range service "php"}}server {{.Address}}:{{.Port}} max_fails=3 fail_timeout=60 weight=1;
            {{else}}server 127.0.0.1:65535{{end}}
    }
    
    server {
            listen   80;
            root /var/www/html;
            index index.php index.html index.htm;
            server_name your.domain.com;
            sendfile off;
            location / {
            }
    
            location ~ \.php$ {
                    fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
                    fastcgi_split_path_info ^(.+\.php)(/.+)$;
                    fastcgi_pass fpm;
                    fastcgi_index index.php;
                    include fastcgi_params;
            }
    }
    

    и запустим Consul Template:
    consul-template -consul <your_consul_ip>:8500 -template server.conf.ctmpl -once -dry
    

    Параметр -dry выводит получившийся конфиг в stdout, параметр -once запустит consul-template один раз.
    upstream fpm {
            least_conn;
            server 127.0.0.1:9000 max_fails=3 fail_timeout=60 weight=1;
    }
    
    server {
            listen   80;
            root /var/www/html;
            index index.php index.html index.htm;
            server_name your.domain.com;
            sendfile off;
            location / {
            }
            location ~ \.php$ {
                    fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
                    fastcgi_split_path_info ^(.+\.php)(/.+)$;
                    fastcgi_pass fpm;
                    fastcgi_index index.php;
                    include fastcgi_params;
            }
    }
    

    Как мы видим, он запрашивает у Consul IP-адреса и порты сервисов под названием php и выводит получившийся из шаблона конфигурационный файл.
    Мы можем поддерживать актуальным конфигурационный файл nginx:
    consul-template -consul <your_consul_ip>:8500 -template server.conf.ctmpl:/etc/nginx/conf.d/server.conf:service nginx reload
    


    Таким образом, Consul Template будет следить за сервисами и передавать их в конфиг nginx. В случае, если сервис вдруг упал или у него сменился порт, Consul Template обновит конфигурационный файл и сделает nginx reload.

    Очень удобно использовать Consul Template для балансировщика (nginx, haproxy).
    Но это всего лишь один из юзкейсов, в котором можно использовать этот замечательный инструмент.

    Подробнее о Consul Template

    Практика





    Итак, мы имеем четыре виртуальных машины на локалхосте, на них установлен Debian 8 Jessie, ядро версии > 3.16 и у нас есть время и желание подробнее ознакомиться с данным стеком технологий и попробовать запустить в кластере какое-нибудь веб-приложение.

    Давайте поднимем на них простой блог wordpress.

    * Здесь мы опускаем момент настройки TLS* между нодами Swarm и Consul.

    Установка окружения на ноды.



    Добавим репозиторий на каждую виртуальную машину (далее — нода)
    echo "deb http://apt.dockerproject.org/repo debian-jessie main" > /etc/apt/sources.list.d/docker.list
    

    И установим необходимые пакеты для нашего окружения.
    apt-get update
    apt-get install ca-certificates
    apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
    apt-get update
    apt-get install docker-engine aufs-tools
    

    Запуск обвязки на primary-ноде:
    docker run --restart=unless-stopped -d -h `hostname` --name consul -v /mnt:/data  \
        -p `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8300:8300 \
        -p `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8301:8301 \
        -p `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8301:8301/udp \
        -p `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8302:8302 \
        -p `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8302:8302/udp \
        -p `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8400:8400 \
        -p 8500:8500 \
        -p 172.17.0.1:53:53/udp \
        gliderlabs/consul-server -server -rejoin -advertise `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'` -bootstrap
    

    Параметр --restart=unless-stopped позволит держать контейнер в запущенном состоянии даже при перезапуске docker-daemon, если он не был остановлен вручную.

    После запуска Consul необходимо подправить параметры запуска docker-daemon в systemd
    В файле /etc/systemd/system/multi-user.target.wants/docker.service строку ExecStart нужно привести к следующему виду:
    ExecStart=/usr/bin/docker daemon -H fd:// -H tcp://<your_ip>:2375 --storage-driver=aufs --cluster-store=consul://<your_ip>:8500 --cluster-advertise <your_ip>:2375
    

    И после этого перезапустить демон
    systemctl daemon-reload
    service docker restart
    

    Проверим, что Consul поднялся и работает:
    docker ps

    Теперь запустим swarm-manager на primary-ноде.
    docker run --restart=unless-stopped -d \
        -p 3375:2375 \
        swarm manage \
        --replication \
        --advertise `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:3375 \
        consul://`ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8500/
    

    Команда manage запустит Swarm manager на ноде.
    Параметр --replication включает репликацию между primary- и secondary- нодами кластера.
    docker run --restart=unless-stopped -d \
        swarm join \
        --advertise=`ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:2375 \
        consul://`ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8500/
    

    Команда join добавит ноду в кластер swarm, на которой мы будем запускать приложения в контейнерах.
    Передав адрес Consul, мы добавим возможность service discovery.

    И, конечно же, Registrator:
    docker run --restart=unless-stopped -d \
        --name=registrator \
        --net=host \
        --volume=/var/run/docker.sock:/tmp/docker.sock \
        gliderlabs/registrator:latest \
        consul://`ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8500
    

    Теперь приступим к остальным нодам.

    Запускаем Consul:
    docker run --restart=unless-stopped -d -h `hostname` --name consul -v /mnt:/data \
        -p `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8300:8300 \
        -p `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8301:8301 \
        -p `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8301:8301/udp \
        -p `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8302:8302 \
        -p `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8302:8302/udp \
        -p `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8400:8400 \
        -p `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8500:8500 \
        -p 172.17.0.1:53:53/udp \
        gliderlabs/consul-server -server -rejoin -advertise `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'` -join <primary_node_ip>
    

    Здесь в параметре -join необходимо указать адрес нашей primary-node, которую мы настраивали выше.

    Swarm manager:
    docker run --restart=unless-stopped -d \
        -p 3375:2375 \
        swarm manage \
        --advertise `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:3375 \
        consul://`ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8500/
    

    Прицепим ноду к кластеру:
    docker run --restart=unless-stopped -d \
        swarm join \
        --advertise=`ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:2375 \
        consul://`ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8500/
    

    И Registrator для регистрации сервисов в Consul.
    docker run --restart=unless-stopped -d \
        --name=registrator \
        --net=host \
        --volume=/var/run/docker.sock:/tmp/docker.sock \
        gliderlabs/registrator:latest \
        -ip `ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'` \
        consul://`ifconfig eth0 | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'`:8500
    


    Немного про «быстрые команды»


    Рестарт всех контейнеров
    docker stop $(docker ps -aq);docker start $(docker ps -aq)

    Удаление всех контейнеров
    docker stop $(docker ps -aq);docker rm $(docker ps -aq)

    Удаление всех неактивных контейнеров:
    docker stop $(docker ps -a | grep 'Exited' | awk '{print $1}') && docker rm $(docker ps -a | grep 'Exited' | awk '{print $1}')

    Удаление всех томов (занятые не удляются)
    docker volume rm $(docker volume ls -q);

    Удаление всех образов (занятые не удляются)
    docker rmi $(docker images -q);


    Frontend



    Итак, наш кластер готов к труду и обороне. Давайте вернёмся на нашу primary-ноду и запустим фронтенд-балансировщик.
    Как я упоминал выше, при работе в командной строке, бывает удобно указать переменную
    export DOCKER_HOST=tcp://<my_swarm_ip>:3375

    и использовать команды docker как обычно, но уже работая не с локальной нодой, а с кластером в целом.

    Мы будем пользоваться образом phusion-baseimage и немного его модифицируем в процессе работы. В него необходимо добавить Consul Template для того, чтобы он поддерживал конфигурационный файл nginx в актуальном состоянии и держал в нем список живых и работающих воркеров. Создаём папку nginx-lb и создаём в ней файл Dockerfile примерно такого содержания:

    Скрытый текст
    FROM phusion/baseimage:0.9.18
    
    ENV NGINX_VERSION 1.8.1-1~trusty
    
    ENV DEBIAN_FRONTEND=noninteractive
    
    # Avoid ERROR: invoke-rc.d: policy-rc.d denied execution of start.
    RUN echo "#!/bin/sh\nexit 0" > /usr/sbin/policy-rc.d
    
    RUN curl -sS http://nginx.org/keys/nginx_signing.key | sudo apt-key add - && \
        echo 'deb http://nginx.org/packages/ubuntu/ trusty nginx' >> /etc/apt/sources.list && \
        echo 'deb-src http://nginx.org/packages/ubuntu/ trusty nginx' >> /etc/apt/sources.list && \
        apt-get update -qq && apt-get install -y unzip ca-certificates nginx=${NGINX_VERSION} && \
        rm -rf /var/lib/apt/lists/* && \
        ln -sf /dev/stdout /var/log/nginx/access.log && \
        ln -sf /dev/stderr /var/log/nginx/error.log
    
    EXPOSE 80
    
    # Скачиваем и распаковываем последнюю версию Consul Template
    ADD https://releases.hashicorp.com/consul-template/0.12.2/consul-template_0.12.2_linux_amd64.zip /usr/bin/
    RUN unzip /usr/bin/consul-template_0.12.2_linux_amd64.zip -d /usr/local/bin
    
    ADD nginx.service /etc/service/nginx/run
    RUN chmod a+x /etc/service/nginx/run
    ADD consul-template.service /etc/service/consul-template/run
    RUN chmod a+x /etc/service/consul-template/run
    
    RUN rm -v /etc/nginx/conf.d/*.conf
    ADD app.conf.ctmpl /etc/consul-templates/app.conf.ctmpl
    
    
    CMD ["/sbin/my_init"]
    



    Теперь нам нужно создать скрипт запуска nignx. Создаём файл nginx.service:
    #!/bin/sh
    
    /usr/sbin/nginx -c /etc/nginx/nginx.conf -t && \
    exec /usr/sbin/nginx -c /etc/nginx/nginx.conf -g "daemon off;"
    

    И скрипт запуска Consul Template:
    #!/bin/sh
    
    exec /usr/local/bin/consul-template \
        -consul consul:8500 \
        -template "/etc/consul-templates/app.conf.ctmpl:/etc/nginx/conf.d/app.conf:sv hup nginx || true"
    

    Отлично. Теперь нам нужен шаблон конфигурационного файла nginx для Consul Template. Создаём app.conf:

    Скрытый текст
    upstream fpm {
            least_conn;
            {{range service "fpm"}}server {{.Address}}:{{.Port}} max_fails=3 fail_timeout=60 weight=1;
            {{else}}server 127.0.0.1:65535{{end}}
    }
    
    server {
    	listen   80;
    	root /var/www/html;
    	index index.php index.html index.htm;
    	server_name domain.example.com;
    	sendfile off;
    	location / {
    		try_files $uri $uri/ /index.php?q=$uri&$args;
    	}
    	location /doc/ {
    		alias /usr/share/doc/;
    		autoindex on;
    		allow 127.0.0.1;
    		allow ::1;
    		deny all;
    	}
    
    	error_page 500 502 503 504 /50x.html;
    	location = /50x.html {
    		root /usr/share/nginx/www;
    	}
    
    	location ~ \.php$ {
                    try_files $uri =404;
    		fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
    		fastcgi_split_path_info ^(.+\.php)(/.+)$;
    		fastcgi_pass fpm;
    		fastcgi_index index.php;
    		include fastcgi_params;
    	}
    
    	location ~ /\.ht {
    		deny all;
    	}
    }
    



    Теперь нам нужно собрать модифицированный образ:
    docker build -t nginx-lb . 

    У нас есть два варианта: собрать этот образ на каждой ноде кластера руками или загрузить его в бесплатное облако Docker Hub, откуда его можно будет взять когда угодно и из любого места без лишних телодвижений. Или же в свой личный Docker Registry.
    Работа с Docker Hub очень подробно описана в
    документации.

    Теперь самое время посмотреть, что получилось. Запускаем контейнер:
    docker run -p 80:80 -v /mnt/storage/www:/var/www/html -d --name balancer --link consul:consul -l "SERVICE_NAME=balancer" -l "SERVICE_TAGS=balancer" \
    -l "SERVICE_CHECK_HTTP=/" -l "SERVICE_CHECK_INTERVAL=15s" -l "SERVICE_CHECK_TIMEOUT=1s" nginx-lb
    

    Проверяем, ткнувшись браузером. Да, он отдаст Bad Gateway, т.к. у нас нет ни статики, ни бэкенда.

    Backend



    Отлично, с фронтендом мы разобрались. Теперь кто-то должен обрабатывать php-код. В этом нам поможет образ WordPress с FPM
    Здесь нам тоже потребуется немного поправить образ. А именно — добавить Consul Template для обнаружения серверов MySQL. Нам же не хочется каждый раз искать на какой ноде запущен сервер базы данных и указывать его адрес вручную при запуске образа? Это занимает не очень много времени, но мы — лентяи, а «лень — двигатель прогресса» (с).

    Dockerfile
    FROM php:5.6-fpm
    
    # install the PHP extensions we need
    RUN apt-get update && apt-get install -y unzip libpng12-dev libjpeg-dev && rm -rf /var/lib/apt/lists/* \
            && docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr \
            && docker-php-ext-install gd mysqli opcache
    
    # set recommended PHP.ini settings
    # see https://secure.php.net/manual/en/opcache.installation.php
    RUN { \
                    echo 'opcache.memory_consumption=128'; \
                    echo 'opcache.interned_strings_buffer=8'; \
                    echo 'opcache.max_accelerated_files=4000'; \
                    echo 'opcache.revalidate_freq=60'; \
                    echo 'opcache.fast_shutdown=1'; \
                    echo 'opcache.enable_cli=1'; \
            } > /usr/local/etc/php/conf.d/opcache-recommended.ini
    
    VOLUME /var/www/html
    
    ENV WORDPRESS_VERSION 4.4.2
    ENV WORDPRESS_SHA1 7444099fec298b599eb026e83227462bcdf312a6
    
    # upstream tarballs include ./wordpress/ so this gives us /usr/src/wordpress
    RUN curl -o wordpress.tar.gz -SL https://wordpress.org/wordpress-${WORDPRESS_VERSION}.tar.gz \
            && echo "$WORDPRESS_SHA1 *wordpress.tar.gz" | sha1sum -c - \
            && tar -xzf wordpress.tar.gz -C /usr/src/ \
            && rm wordpress.tar.gz \
            && chown -R www-data:www-data /usr/src/wordpress
    
    ADD https://releases.hashicorp.com/consul-template/0.12.2/consul-template_0.12.2_linux_amd64.zip /usr/bin/
    RUN unzip /usr/bin/consul-template_0.12.2_linux_amd64.zip -d /usr/local/bin
    # Добавляем шаблон настроек БД.
    ADD db.conf.php.ctmpl /db.conf.php.ctmpl
    # Добавляем скрипт запуска consul-template
    ADD consul-template.sh /usr/local/bin/consul-template.sh
    # Добавляем шаблон обнаружения MySQL для создания базы при установке WP
    ADD mysql.ctmpl /tmp/mysql.ctmpl
    
    COPY docker-entrypoint.sh /entrypoint.sh
    
    # grr, ENTRYPOINT resets CMD now
    ENTRYPOINT ["/entrypoint.sh"]
    CMD ["php-fpm"]
    



    Создаём шаблон настроек MySQL db.conf.php.ctmpl:
    <?php
    {{range service "mysql"}}
    define('DB_HOST', '{{.Address}}');
    {{else}}
    define('DB_HOST', 'mysql');
    {{end}}
    ?>
    

    И скрипт запуска consul-template.sh:
    #!/bin/sh
    echo "Starting Consul Template"
    
    exec /usr/local/bin/consul-template \
        -consul consul:8500 \
        -template "/db.conf.php.ctmpl:/var/www/html/db.conf.php"
    


    Шаблон обнаружения сервера MySQL mysql.ctmpl:
    {{range service "mysql"}}{{.Address}} {{.Port}} {{end}}
    

    В скрипте docker-entrypoint.sh нам стоит поправить несколько вещей. А именно — подключить Consul Template для обнаружения сервера MySQL и перевесить fpm на 0.0.0.0, т. к. по-умолчанию он слушает только 127.0.0.1:

    Скрытый текст
    #!/bin/bash
    set -e
    
    # Обнаруживаем хост БД
    WORDPRESS_DB_HOST="$(/usr/local/bin/consul-template --template=/tmp/mysql-master.ctmpl --consul=consul:8500 --dry -once | awk '{print $1}' | tail -1)"
    # Обнаружаем порт БД
    WORDPRESS_DB_PORT="$(/usr/local/bin/consul-template --template=/tmp/mysql-master.ctmpl --consul=consul:8500 --dry -once | awk '{print $2}' | tail -1)"
    
    if [[ "$1" == apache2* ]] || [ "$1" == php-fpm ]; then
    	if [ -n "$MYSQL_PORT_3306_TCP" ]; then
    		if [ -z "$WORDPRESS_DB_HOST" ]; then
    			WORDPRESS_DB_HOST='mysql'
    		else
    			echo >&2 'warning: both WORDPRESS_DB_HOST and MYSQL_PORT_3306_TCP found'
    			echo >&2 "  Connecting to WORDPRESS_DB_HOST ($WORDPRESS_DB_HOST)"
    			echo >&2 '  instead of the linked mysql container'
    		fi
    	fi
    
    	if [ -z "$WORDPRESS_DB_HOST" ]; then
    		echo >&2 'error: missing WORDPRESS_DB_HOST and MYSQL_PORT_3306_TCP environment variables'
    		echo >&2 '  Did you forget to --link some_mysql_container:mysql or set an external db'
    		echo >&2 '  with -e WORDPRESS_DB_HOST=hostname:port?'
    		exit 1
    	fi
    
    	# if we're linked to MySQL and thus have credentials already, let's use them
    	: ${WORDPRESS_DB_USER:=${MYSQL_ENV_MYSQL_USER:-root}}
    	if [ "$WORDPRESS_DB_USER" = 'root' ]; then
    		: ${WORDPRESS_DB_PASSWORD:=$MYSQL_ENV_MYSQL_ROOT_PASSWORD}
    	fi
    	: ${WORDPRESS_DB_PASSWORD:=$MYSQL_ENV_MYSQL_PASSWORD}
    	: ${WORDPRESS_DB_NAME:=${MYSQL_ENV_MYSQL_DATABASE:-wordpress}}
    
    	if [ -z "$WORDPRESS_DB_PASSWORD" ]; then
    		echo >&2 'error: missing required WORDPRESS_DB_PASSWORD environment variable'
    		echo >&2 '  Did you forget to -e WORDPRESS_DB_PASSWORD=... ?'
    		echo >&2
    		echo >&2 '  (Also of interest might be WORDPRESS_DB_USER and WORDPRESS_DB_NAME.)'
    		exit 1
    	fi
    
    	if ! [ -e index.php -a -e wp-includes/version.php ]; then
    		echo >&2 "WordPress not found in $(pwd) - copying now..."
    		if [ "$(ls -A)" ]; then
    			echo >&2 "WARNING: $(pwd) is not empty - press Ctrl+C now if this is an error!"
    			( set -x; ls -A; sleep 10 )
    		fi
    		tar cf - --one-file-system -C /usr/src/wordpress . | tar xf -
    		echo >&2 "Complete! WordPress has been successfully copied to $(pwd)"
    		if [ ! -e .htaccess ]; then
    			# NOTE: The "Indexes" option is disabled in the php:apache base image
    			cat > .htaccess <<-'EOF'
    				# BEGIN WordPress
    				<IfModule mod_rewrite.c>
    				RewriteEngine On
    				RewriteBase /
    				RewriteRule ^index\.php$ - [L]
    				RewriteCond %{REQUEST_FILENAME} !-f
    				RewriteCond %{REQUEST_FILENAME} !-d
    				RewriteRule . /index.php [L]
    				</IfModule>
    				# END WordPress
    			EOF
    			chown www-data:www-data .htaccess
    		fi
    	fi
    
    	# TODO handle WordPress upgrades magically in the same way, but only if wp-includes/version.php's $wp_version is less than /usr/src/wordpress/wp-includes/version.php's $wp_version
    
    	# version 4.4.1 decided to switch to windows line endings, that breaks our seds and awks
    	# https://github.com/docker-library/wordpress/issues/116
    	# https://github.com/WordPress/WordPress/commit/1acedc542fba2482bab88ec70d4bea4b997a92e4
    	sed -ri 's/\r\n|\r/\n/g' wp-config*
    
    	# FPM должен слушать  0.0.0.0
    	sed -i 's/listen = 127.0.0.1:9000/listen = 0.0.0.0:9000/g' /usr/local/etc/php-fpm.d/www.conf
    
    	if [ ! -e wp-config.php ]; then
    		awk '/^\/\*.*stop editing.*\*\/$/ && c == 0 { c = 1; system("cat") } { print }' wp-config-sample.php > wp-config.php <<'EOPHP'
    
    
    // If we're behind a proxy server and using HTTPS, we need to alert Wordpress of that fact
    // see also http://codex.wordpress.org/Administration_Over_SSL#Using_a_Reverse_Proxy
    if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
    	$_SERVER['HTTPS'] = 'on';
    }
    
    EOPHP
    		# Инклудим сгенерированный Consul Template конфиг с обнаруженным MySQL
                    DB_HOST_PRE=$(grep 'DB_HOST' wp-config.php)
                    sed -i "s/$DB_HOST_PRE/include 'db.conf.php';/g" wp-config.php
    		chown www-data:www-data wp-config.php
    	fi
    
    	# see http://stackoverflow.com/a/2705678/433558
    	sed_escape_lhs() {
    		echo "$@" | sed 's/[]\/$*.^|[]/\\&/g'
    	}
    	sed_escape_rhs() {
    		echo "$@" | sed 's/[\/&]/\\&/g'
    	}
    	php_escape() {
    		php -r 'var_export(('$2') $argv[1]);' "$1"
    	}
    	set_config() {
    		key="$1"
    		value="$2"
    		var_type="${3:-string}"
    		start="(['\"])$(sed_escape_lhs "$key")\2\s*,"
    		end="\);"
    		if [ "${key:0:1}" = '$' ]; then
    			start="^(\s*)$(sed_escape_lhs "$key")\s*="
    			end=";"
    		fi
    		sed -ri "s/($start\s*).*($end)$/\1$(sed_escape_rhs "$(php_escape "$value" "$var_type")")\3/" wp-config.php
    	}
    
    	set_config 'DB_HOST' "$WORDPRESS_DB_HOST"
    	set_config 'DB_USER' "$WORDPRESS_DB_USER"
    	set_config 'DB_PASSWORD' "$WORDPRESS_DB_PASSWORD"
    	set_config 'DB_NAME' "$WORDPRESS_DB_NAME"
    
    	# allow any of these "Authentication Unique Keys and Salts." to be specified via
    	# environment variables with a "WORDPRESS_" prefix (ie, "WORDPRESS_AUTH_KEY")
    	UNIQUES=(
    		AUTH_KEY
    		SECURE_AUTH_KEY
    		LOGGED_IN_KEY
    		NONCE_KEY
    		AUTH_SALT
    		SECURE_AUTH_SALT
    		LOGGED_IN_SALT
    		NONCE_SALT
    	)
    	for unique in "${UNIQUES[@]}"; do
    		eval unique_value=\$WORDPRESS_$unique
    		if [ "$unique_value" ]; then
    			set_config "$unique" "$unique_value"
    		else
    			# if not specified, let's generate a random value
    			current_set="$(sed -rn "s/define\((([\'\"])$unique\2\s*,\s*)(['\"])(.*)\3\);/\4/p" wp-config.php)"
    			if [ "$current_set" = 'put your unique phrase here' ]; then
    				set_config "$unique" "$(head -c1M /dev/urandom | sha1sum | cut -d' ' -f1)"
    			fi
    		fi
    	done
    
    	if [ "$WORDPRESS_TABLE_PREFIX" ]; then
    		set_config '$table_prefix' "$WORDPRESS_TABLE_PREFIX"
    	fi
    
    	if [ "$WORDPRESS_DEBUG" ]; then
    		set_config 'WP_DEBUG' 1 boolean
    	fi
    
    	TERM=dumb php -- "$WORDPRESS_DB_HOST" "$WORDPRESS_DB_USER" "$WORDPRESS_DB_PASSWORD" "$WORDPRESS_DB_NAME" <<'EOPHP'
    <?php
    // database might not exist, so let's try creating it (just to be safe)
    
    $stderr = fopen('php://stderr', 'w');
    
    list($host, $port) = explode(':', $argv[1], 2);
    
    $maxTries = 10;
    do {
    	$mysql = new mysqli($host, $argv[2], $argv[3], '', (int)$port);
    	if ($mysql->connect_error) {
    		fwrite($stderr, "\n" . 'MySQL Connection Error: (' . $mysql->connect_errno . ') ' . $mysql->connect_error . "\n");
    		--$maxTries;
    		if ($maxTries <= 0) {
    			exit(1);
    		}
    		sleep(3);
    	}
    } while ($mysql->connect_error);
    
    if (!$mysql->query('CREATE DATABASE IF NOT EXISTS `' . $mysql->real_escape_string($argv[4]) . '`')) {
    	fwrite($stderr, "\n" . 'MySQL "CREATE DATABASE" Error: ' . $mysql->error . "\n");
    	$mysql->close();
    	exit(1);
    }
    
    $mysql->close();
    EOPHP
    fi
    
    # Инклудим consul-template
    exec /usr/local/sbin/php-fpm &
    exec /usr/local/bin/consul-template.sh
    exec "$@"
    



    Хорошо, теперь соберём образ:
    docker build -t fpm .
    

    Запускать его пока что не стоит, т. к. у нас еще нет сервера базы данных для полноценной работы Wordpress
    docker run --name fpm.0 -d -v /mnt/storage/www:/var/www/html \
    -e WORDPRESS_DB_NAME=wordpressp -e WORDPRESS_DB_USER=wordpress -e WORDPRESS_DB_PASSWORD=wordpress \
    --link consul:consul -l "SERVICE_NAME=php-fpm" -l "SERVICE_PORT=9000" -p 9000:9000 fpm
    


    База данных:



    Master



    В качестве базы данных воспользуемся образом MySQL 5.7.

    Нам потребуется также его немного поправить. А именно: сделать два образа. Один — для Master, второй — для Slave.
    Начнём с образа для Master.

    Наш Dockerfile
    FROM debian:jessie
    
    # add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added
    RUN groupadd -r mysql && useradd -r -g mysql mysql
    
    RUN mkdir /docker-entrypoint-initdb.d
    
    # FATAL ERROR: please install the following Perl modules before executing /usr/local/mysql/scripts/mysql_install_db:
    # File::Basename
    # File::Copy
    # Sys::Hostname
    # Data::Dumper
    RUN apt-get update && apt-get install -y perl pwgen --no-install-recommends && rm -rf /var/lib/apt/lists/*
    
    # gpg: key 5072E1F5: public key "MySQL Release Engineering <mysql-build@oss.oracle.com>" imported
    RUN apt-key adv --keyserver ha.pool.sks-keyservers.net --recv-keys A4A9406876FCBD3C456770C88C718D3B5072E1F5
    
    ENV MYSQL_MAJOR 5.7
    ENV MYSQL_VERSION 5.7.11-1debian8
    
    RUN echo "deb http://repo.mysql.com/apt/debian/ jessie mysql-${MYSQL_MAJOR}" > /etc/apt/sources.list.d/mysql.list
    
    # the "/var/lib/mysql" stuff here is because the mysql-server postinst doesn't have an explicit way to disable the mysql_install_db codepath besides having a database already "configured" (ie, stuff in /var/lib/mysql/mysql)
    # also, we set debconf keys to make APT a little quieter
    RUN { \
    		echo mysql-community-server mysql-community-server/data-dir select ''; \
    		echo mysql-community-server mysql-community-server/root-pass password ''; \
    		echo mysql-community-server mysql-community-server/re-root-pass password ''; \
    		echo mysql-community-server mysql-community-server/remove-test-db select false; \
    	} | debconf-set-selections \
    	&& apt-get update && apt-get install -y mysql-server="${MYSQL_VERSION}" && rm -rf /var/lib/apt/lists/* \
    	&& rm -rf /var/lib/mysql && mkdir -p /var/lib/mysql
    
    # comment out a few problematic configuration values
    # don't reverse lookup hostnames, they are usually another container
    RUN sed -Ei 's/^(bind-address|log)/#&/' /etc/mysql/my.cnf \
    	&& echo 'skip-host-cache\nskip-name-resolve' | awk '{ print } $1 == "[mysqld]" && c == 0 { c = 1; system("cat") }' /etc/mysql/my.cnf > /tmp/my.cnf \
    	&& mv /tmp/my.cnf /etc/mysql/my.cnf 
    
    
    VOLUME /var/lib/mysql
    
    COPY docker-entrypoint.sh /entrypoint.sh
    ENTRYPOINT ["/entrypoint.sh"]
    
    EXPOSE 3306
    CMD ["mysqld"]
    



    Скрипт запуска MySQL:

    docker-entrypoint.sh
    #!/bin/bash
    set -eo pipefail
    # if command starts with an option, prepend mysqld
    if [ "${1:0:1}" = '-' ]; then
    	set -- mysqld "$@"
    fi
    
    if [ "$1" = 'mysqld' ]; then
    	# Get config
    	DATADIR="$("$@" --verbose --help 2>/dev/null | awk '$1 == "datadir" { print $2; exit }')"
    
    	if [ ! -d "$DATADIR/mysql" ]; then
    		if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" -a -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then
    			echo >&2 'error: database is uninitialized and password option is not specified '
    			echo >&2 '  You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD'
    			exit 1
    		fi
    
    		mkdir -p "$DATADIR"
    		chown -R mysql:mysql "$DATADIR"
    
    		echo 'Initializing database'
    		"$@" --initialize-insecure
    		echo 'Database initialized'
    
    		"$@" --skip-networking &
    		pid="$!"
    
    		mysql=( mysql --protocol=socket -uroot )
    
    		for i in {30..0}; do
    			if echo 'SELECT 1' | "${mysql[@]}" &> /dev/null; then
    				break
    			fi
    			echo 'MySQL init process in progress...'
    			sleep 1
    		done
    		if [ "$i" = 0 ]; then
    			echo >&2 'MySQL init process failed.'
    			exit 1
    		fi
    
    		if [ -n "${REPLICATION_MASTER}" ]; then
    			echo "=> Configuring MySQL replication as master (1/2) ..."
    			if [ ! -f /replication_set.1 ]; then
    		        	echo "=> Writting configuration file /etc/mysql/my.cnf with server-id=1"
    			        echo 'server-id = 1' >> /etc/mysql/my.cnf
    			        echo 'log-bin = mysql-bin' >> /etc/mysql/my.cnf
    		        	touch /replication_set.1
    	    		else
    		        	echo "=> MySQL replication master already configured, skip"
    			fi
    		fi
    		# Set MySQL REPLICATION - SLAVE
    		if [ -n "${REPLICATION_SLAVE}" ]; then
    		    echo "=> Configuring MySQL replication as slave (1/2) ..."
    		    if [ -n "${MYSQL_PORT_3306_TCP_ADDR}" ] && [ -n "${MYSQL_PORT_3306_TCP_PORT}" ]; then
    		        if [ ! -f /replication_set.1 ]; then
    		            echo "=> Writting configuration file /etc/mysql/my.cnf with server-id=2"
    		            echo 'server-id = 2' >> /etc/mysql/my.cnf
    		            echo 'log-bin = mysql-bin' >> /etc/mysql/my.cnf
                        echo 'log-bin=slave-bin' >> /etc/mysql/my.cnf
    		            touch /replication_set.1
    		        else
    		            echo "=> MySQL replication slave already configured, skip"
    		        fi
    		    else
    		        echo "=> Cannot configure slave, please link it to another MySQL container with alias as 'mysql'"
    		        exit 1
    		    fi
    		fi
    
    		# Set MySQL REPLICATION - SLAVE
    		if [ -n "${REPLICATION_SLAVE}" ]; then
    		    echo "=> Configuring MySQL replication as slave (2/2) ..."
    		    if [ -n "${MYSQL_PORT_3306_TCP_ADDR}" ] && [ -n "${MYSQL_PORT_3306_TCP_PORT}" ]; then
    		        if [ ! -f /replication_set.2 ]; then
    		            echo "=> Setting master connection info on slave"
    			echo "!!! DEBUG: ${REPLICATION_USER}, ${REPLICATION_PASS}."
    				"${mysql[@]}" <<-EOSQL
    					-- What's done in this file shouldn't be replicated
    					--  or products like mysql-fabric won't work
    					SET @@SESSION.SQL_LOG_BIN=0;
    					CHANGE MASTER TO MASTER_HOST='${MYSQL_PORT_3306_TCP_ADDR}',MASTER_USER='${REPLICATION_USER}',MASTER_PASSWORD='${REPLICATION_PASS}',MASTER_PORT=${MYSQL_PORT_3306_TCP_PORT}, MASTER_CONNECT_RETRY=30;
    					START SLAVE ;
    				EOSQL
    
    		            echo "=> Done!"
    		            touch /replication_set.2
    		        else
    		            echo "=> MySQL replication slave already configured, skip"
    		        fi
    		    else
    		        echo "=> Cannot configure slave, please link it to another MySQL container with alias as 'mysql'"
    		        exit 1
    		    fi
    		fi
    
    
    		if [ -z "$MYSQL_INITDB_SKIP_TZINFO" ]; then
    			# sed is for https://bugs.mysql.com/bug.php?id=20545
    			mysql_tzinfo_to_sql /usr/share/zoneinfo | sed 's/Local time zone must be set--see zic manual page/FCTY/' | "${mysql[@]}" mysql
    		fi
    
    		if [ ! -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then
    			MYSQL_ROOT_PASSWORD="$(pwgen -1 32)"
    			echo "GENERATED ROOT PASSWORD: $MYSQL_ROOT_PASSWORD"
    		fi
    		"${mysql[@]}" <<-EOSQL
    			-- What's done in this file shouldn't be replicated
    			--  or products like mysql-fabric won't work
    			SET @@SESSION.SQL_LOG_BIN=0;
    
    			DELETE FROM mysql.user ;
    			CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ;
    			GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ;
    			DROP DATABASE IF EXISTS test ;
    			FLUSH PRIVILEGES ;
    		EOSQL
    
    		if [ ! -z "$MYSQL_ROOT_PASSWORD" ]; then
    			mysql+=( -p"${MYSQL_ROOT_PASSWORD}" )
    		fi
    
    		# Set MySQL REPLICATION - MASTER
    		if [ -n "${REPLICATION_MASTER}" ]; then
    		    echo "=> Configuring MySQL replication as master (2/2) ..."
    		    if [ ! -f /replication_set.2 ]; then
    		        echo "=> Creating a log user ${REPLICATION_USER}:${REPLICATION_PASS}"
    
    				"${mysql[@]}" <<-EOSQL
    					-- What's done in this file shouldn't be replicated
    					--  or products like mysql-fabric won't work
    					SET @@SESSION.SQL_LOG_BIN=0;
    
    					CREATE USER '${REPLICATION_USER}'@'%' IDENTIFIED BY '${REPLICATION_PASS}';
    					GRANT REPLICATION SLAVE ON *.* TO '${REPLICATION_USER}'@'%' ;
    					FLUSH PRIVILEGES ;
    					RESET MASTER ;
    				EOSQL
    
    		        echo "=> Done!"
    		        touch /replication_set.2
    		    else
    		        echo "=> MySQL replication master already configured, skip"
    		    fi
    		fi
    
    
    		if [ "$MYSQL_DATABASE" ]; then
    			echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" | "${mysql[@]}"
    			mysql+=( "$MYSQL_DATABASE" )
    		fi
    
    		if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then
    			echo "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' ;" | "${mysql[@]}"
    
    			if [ "$MYSQL_DATABASE" ]; then
    				echo "GRANT ALL ON \`$MYSQL_DATABASE\`.* TO '$MYSQL_USER'@'%' ;" | "${mysql[@]}"
    			fi
    
    			echo 'FLUSH PRIVILEGES ;' | "${mysql[@]}"
    		fi
    
    		echo
    		for f in /docker-entrypoint-initdb.d/*; do
    			case "$f" in
    				*.sh)     echo "$0: running $f"; . "$f" ;;
    				*.sql)    echo "$0: running $f"; "${mysql[@]}" < "$f"; echo ;;
    				*.sql.gz) echo "$0: running $f"; gunzip -c "$f" | "${mysql[@]}"; echo ;;
    				*)        echo "$0: ignoring $f" ;;
    			esac
    			echo
    		done
    
    		if [ ! -z "$MYSQL_ONETIME_PASSWORD" ]; then
    			"${mysql[@]}" <<-EOSQL
    				ALTER USER 'root'@'%' PASSWORD EXPIRE;
    			EOSQL
    		fi
    		if ! kill -s TERM "$pid" || ! wait "$pid"; then
    			echo >&2 'MySQL init process failed.'
    			exit 1
    		fi
    
    		echo
    		echo 'MySQL init process done. Ready for start up.'
    		echo
    	fi
    
    	chown -R mysql:mysql "$DATADIR"
    fi
    
    exec "$@"
    



    И сборка:
    docker build -t mysql-master .
    

    docker run --name mysql-master.0 -v /mnt/volumes/master:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=rootpass -e MYSQL_USER=wordpress -e MYSQL_PASSWORD=wordpress -e MYSQL_DB=wordpress  -e REPLICATION_MASTER=true -e REPLICATION_USER=replica -e REPLICATION_PASS=replica --link consul:consul -l "SERVICE_NAME=master" -l "SERVICE_PORT=3306" -p 3306:3306 -d mysql-master
    


    Если Вы заметили, мы добавили в скрипт возможность передавать параметры запуска для настройки репликации MySQL (REPLICATION_USER, REPLICATION_PASS, REPLICATION_MASTER, REPLICATION_SLAVE).

    Slave



    Образ Slave мы сделаем таким образом, чтобы MySQL сам находил Master-сервер и включал репликацию. Здесь опять же к нам на помощь приходит Consul Template:

    Dockerfile
    FROM debian:jessie
    
    # add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added
    RUN groupadd -r mysql && useradd -r -g mysql mysql
    
    RUN mkdir /docker-entrypoint-initdb.d
    
    # FATAL ERROR: please install the following Perl modules before executing /usr/local/mysql/scripts/mysql_install_db:
    # File::Basename
    # File::Copy
    # Sys::Hostname
    # Data::Dumper
    RUN apt-get update && apt-get install -y perl pwgen --no-install-recommends && rm -rf /var/lib/apt/lists/*
    
    # gpg: key 5072E1F5: public key "MySQL Release Engineering <mysql-build@oss.oracle.com>" imported
    RUN apt-key adv --keyserver ha.pool.sks-keyservers.net --recv-keys A4A9406876FCBD3C456770C88C718D3B5072E1F5
    
    ENV MYSQL_MAJOR 5.7
    ENV MYSQL_VERSION 5.7.11-1debian8
    
    RUN echo "deb http://repo.mysql.com/apt/debian/ jessie mysql-${MYSQL_MAJOR}" > /etc/apt/sources.list.d/mysql.list
    
    # the "/var/lib/mysql" stuff here is because the mysql-server postinst doesn't have an explicit way to disable the mysql_install_db codepath besides having a database already "configured" (ie, stuff in /var/lib/mysql/mysql)
    # also, we set debconf keys to make APT a little quieter
    RUN { \
    		echo mysql-community-server mysql-community-server/data-dir select ''; \
    		echo mysql-community-server mysql-community-server/root-pass password ''; \
    		echo mysql-community-server mysql-community-server/re-root-pass password ''; \
    		echo mysql-community-server mysql-community-server/remove-test-db select false; \
    	} | debconf-set-selections \
    	&& apt-get update && apt-get install -y mysql-server="${MYSQL_VERSION}" && rm -rf /var/lib/apt/lists/* \
    	&& rm -rf /var/lib/mysql && mkdir -p /var/lib/mysql
    
    # comment out a few problematic configuration values
    # don't reverse lookup hostnames, they are usually another container
    RUN sed -Ei 's/^(bind-address|log)/#&/' /etc/mysql/my.cnf \
    	&& echo 'skip-host-cache\nskip-name-resolve' | awk '{ print } $1 == "[mysqld]" && c == 0 { c = 1; system("cat") }' /etc/mysql/my.cnf > /tmp/my.cnf \
     	&& mv /tmp/my.cnf /etc/mysql/my.cnf
    
    ADD https://releases.hashicorp.com/consul-template/0.12.2/consul-template_0.12.2_linux_amd64.zip /usr/bin/
    RUN unzip /usr/bin/consul-template_0.12.2_linux_amd64.zip -d /usr/local/bin
    
    ADD mysql-master.ctmpl /tmp/mysql-master.ctmpl
    
    VOLUME /var/lib/mysql
    
    COPY docker-entrypoint.sh /entrypoint.sh
    ENTRYPOINT ["/entrypoint.sh"]
    
    EXPOSE 3306
    CMD ["mysqld"]
    



    docker-entrypoint.sh
    #!/bin/bash
    set -eo pipefail
    
    # Спрашиваем у Consul, где у нас живой master
    MYSQL_PORT_3306_TCP_ADDR="$(/usr/bin/consul-template --template=/tmp/mysql-master.ctmpl --consul=consul:8500 --dry -once | awk '{print $1}' | tail -1)"
    MYSQL_PORT_3306_TCP_PORT="$(/usr/bin/consul-template --template=/tmp/mysql-master.ctmpl --consul=consul:8500 --dry -once | awk '{print $2}' | tail -1)"
    
    if [ "${1:0:1}" = '-' ]; then
    	set -- mysqld "$@"
    fi
    
    if [ "$1" = 'mysqld' ]; then
    	# Get config
    	DATADIR="$("$@" --verbose --help 2>/dev/null | awk '$1 == "datadir" { print $2; exit }')"
    
    	if [ ! -d "$DATADIR/mysql" ]; then
    		if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" -a -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then
    			echo >&2 'error: database is uninitialized and password option is not specified '
    			echo >&2 '  You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD'
    			exit 1
    		fi
    
    		mkdir -p "$DATADIR"
    		chown -R mysql:mysql "$DATADIR"
    
    		echo 'Initializing database'
    		"$@" --initialize-insecure
    		echo 'Database initialized'
    
    		"$@" --skip-networking &
    		pid="$!"
    
    		mysql=( mysql --protocol=socket -uroot )
    
    		for i in {30..0}; do
    			if echo 'SELECT 1' | "${mysql[@]}" &> /dev/null; then
    				break
    			fi
    			echo 'MySQL init process in progress...'
    			sleep 1
    		done
    		if [ "$i" = 0 ]; then
    			echo >&2 'MySQL init process failed.'
    			exit 1
    		fi
    
    		if [ -n "${REPLICATION_MASTER}" ]; then
    			echo "=> Configuring MySQL replication as master (1/2) ..."
    			if [ ! -f /replication_set.1 ]; then
    		        	echo "=> Writting configuration file /etc/mysql/my.cnf with server-id=1"
    			        echo 'server-id = 1' >> /etc/mysql/my.cnf
    			        echo 'log-bin = mysql-bin' >> /etc/mysql/my.cnf
    		        	touch /replication_set.1
    	    		else
    		        	echo "=> MySQL replication master already configured, skip"
    			fi
    		fi
    		# Set MySQL REPLICATION - SLAVE
    		if [ -n "${REPLICATION_SLAVE}" ]; then
    		    echo "=> Configuring MySQL replication as slave (1/2) ..."
    		    if [ -n "${MYSQL_PORT_3306_TCP_ADDR}" ] && [ -n "${MYSQL_PORT_3306_TCP_PORT}" ]; then
    		        if [ ! -f /replication_set.1 ]; then
    		            echo "=> Writting configuration file /etc/mysql/my.cnf with server-id=2"
    		            echo 'server-id = 2' >> /etc/mysql/my.cnf
    		            echo 'log-bin = mysql-bin' >> /etc/mysql/my.cnf
                        echo 'log-bin=slave-bin' >> /etc/mysql/my.cnf
    		            touch /replication_set.1
    		        else
    		            echo "=> MySQL replication slave already configured, skip"
    		        fi
    		    else
    		        echo "=> Cannot configure slave, please link it to another MySQL container with alias as 'mysql'"
    		        exit 1
    		    fi
    		fi
    
    		# Set MySQL REPLICATION - SLAVE
    		if [ -n "${REPLICATION_SLAVE}" ]; then
    		    echo "=> Configuring MySQL replication as slave (2/2) ..."
    		    if [ -n "${MYSQL_PORT_3306_TCP_ADDR}" ] && [ -n "${MYSQL_PORT_3306_TCP_PORT}" ]; then
    		        if [ ! -f /replication_set.2 ]; then
    		            echo "=> Setting master connection info on slave"
    				"${mysql[@]}" <<-EOSQL
    					-- What's done in this file shouldn't be replicated
    					--  or products like mysql-fabric won't work
    					SET @@SESSION.SQL_LOG_BIN=0;
    					CHANGE MASTER TO MASTER_HOST='${MYSQL_PORT_3306_TCP_ADDR}',MASTER_USER='${REPLICATION_USER}',MASTER_PASSWORD='${REPLICATION_PASS}',MASTER_PORT=${MYSQL_PORT_3306_TCP_PORT}, MASTER_CONNECT_RETRY=30;
    					START SLAVE ;
    				EOSQL
    
    		            echo "=> Done!"
    		            touch /replication_set.2
    		        else
    		            echo "=> MySQL replication slave already configured, skip"
    		        fi
    		    else
    		        echo "=> Cannot configure slave, please link it to another MySQL container with alias as 'mysql'"
    		        exit 1
    		    fi
    		fi
    
    
    		if [ -z "$MYSQL_INITDB_SKIP_TZINFO" ]; then
    			# sed is for https://bugs.mysql.com/bug.php?id=20545
    			mysql_tzinfo_to_sql /usr/share/zoneinfo | sed 's/Local time zone must be set--see zic manual page/FCTY/' | "${mysql[@]}" mysql
    		fi
    
    		if [ ! -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then
    			MYSQL_ROOT_PASSWORD="$(pwgen -1 32)"
    			echo "GENERATED ROOT PASSWORD: $MYSQL_ROOT_PASSWORD"
    		fi
    		"${mysql[@]}" <<-EOSQL
    			-- What's done in this file shouldn't be replicated
    			--  or products like mysql-fabric won't work
    			SET @@SESSION.SQL_LOG_BIN=0;
    
    			DELETE FROM mysql.user ;
    			CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ;
    			GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ;
    			DROP DATABASE IF EXISTS test ;
    			FLUSH PRIVILEGES ;
    		EOSQL
    
    		if [ ! -z "$MYSQL_ROOT_PASSWORD" ]; then
    			mysql+=( -p"${MYSQL_ROOT_PASSWORD}" )
    		fi
    
    		# Set MySQL REPLICATION - MASTER
    		if [ -n "${REPLICATION_MASTER}" ]; then
    		    echo "=> Configuring MySQL replication as master (2/2) ..."
    		    if [ ! -f /replication_set.2 ]; then
    		        echo "=> Creating a log user ${REPLICATION_USER}:${REPLICATION_PASS}"
    
    				"${mysql[@]}" <<-EOSQL
    					-- What's done in this file shouldn't be replicated
    					--  or products like mysql-fabric won't work
    					SET @@SESSION.SQL_LOG_BIN=0;
    
    					CREATE USER '${REPLICATION_USER}'@'%' IDENTIFIED BY '${REPLICATION_PASS}';
    					GRANT REPLICATION SLAVE ON *.* TO '${REPLICATION_USER}'@'%' ;
    					FLUSH PRIVILEGES ;
    					RESET MASTER ;
    				EOSQL
    
    		        echo "=> Done!"
    		        touch /replication_set.2
    		    else
    		        echo "=> MySQL replication master already configured, skip"
    		    fi
    		fi
    
    
    		if [ "$MYSQL_DATABASE" ]; then
    			echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" | "${mysql[@]}"
    			mysql+=( "$MYSQL_DATABASE" )
    		fi
    
    		if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then
    			echo "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' ;" | "${mysql[@]}"
    
    			if [ "$MYSQL_DATABASE" ]; then
    				echo "GRANT ALL ON \`$MYSQL_DATABASE\`.* TO '$MYSQL_USER'@'%' ;" | "${mysql[@]}"
    			fi
    
    			echo 'FLUSH PRIVILEGES ;' | "${mysql[@]}"
    		fi
    
    		echo
    		for f in /docker-entrypoint-initdb.d/*; do
    			case "$f" in
    				*.sh)     echo "$0: running $f"; . "$f" ;;
    				*.sql)    echo "$0: running $f"; "${mysql[@]}" < "$f"; echo ;;
    				*.sql.gz) echo "$0: running $f"; gunzip -c "$f" | "${mysql[@]}"; echo ;;
    				*)        echo "$0: ignoring $f" ;;
    			esac
    			echo
    		done
    
    		if [ ! -z "$MYSQL_ONETIME_PASSWORD" ]; then
    			"${mysql[@]}" <<-EOSQL
    				ALTER USER 'root'@'%' PASSWORD EXPIRE;
    			EOSQL
    		fi
    		if ! kill -s TERM "$pid" || ! wait "$pid"; then
    			echo >&2 'MySQL init process failed.'
    			exit 1
    		fi
    
    		echo
    		echo 'MySQL init process done. Ready for start up.'
    		echo
    	fi
    
    	chown -R mysql:mysql "$DATADIR"
    fi
    
    exec "$@"
    



    И шаблон для Consul Template, mysql-master.ctmpl:
    {{range service "master"}}{{.Address}} {{.Port}} {{end}}
    

    Собираем:
    docker build -t mysql-slave .
    

    Запускаем:
    docker run --name mysql-slave.0 -v /mnt/volumes/slave:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=rootpass -e REPLICATION_SLAVE=true -e REPLICATION_USER=replica -e REPLICATION_PASS=replica --link=consul:consul -l "SERVICE_NAME=slave" -l "SERVICE_PORT=3307" -p 3307:3306 -d mysql-slave
    

    Итак, теперь самое время запустить наш бэкенд.
    docker run --name fpm.0 -d -v /mnt/storage/www:/var/www/html \
    -e WORDPRESS_DB_NAME=wordpressp -e WORDPRESS_DB_USER=wordpress -e WORDPRESS_DB_PASSWORD=wordpress \
    --link consul:consul -l "SERVICE_NAME=php-fpm" -l "SERVICE_PORT=9000" -l "SERVICE_TAGS=worker" -p 9000:9000 fpm
    

    Если всё прошло удачно, то, открыв в браузере адрес нашего балансировщика, мы увидим приветствие Wordress с предложением установить его.
    В противном случае — смотрим логи
    docker logs <container_name>
    


    Docker-compose.



    Мы собрали образы с сервисами, необходимыми для нашего приложения, мы можем запускать его в любое время в любом месте, но… Зачем нам помнить столько команд, параметров запуска, переменных для запуска контейнеров? Здесь к нам на помощь приходит ещё один классный инструмент — docker-compose.
    Этот инструмент предназначен для запуска приложений в нескольких контейнерах. Docker-compose использует декларативный сценарий в формате YAML, в котором указываются с какими параметрами и переменными запустить контейнер. Сценарии легко читаются и просты для восприятия.

    Мы напишем такой простой сценарий, который запустит в нескольких контейнерах всё необходимое для нашего веб-приложения docker-compose.yml.

    Скрытый текст
    mysql-master:
          image: mysql-master
          ports:
            - "3306:3306"
          environment:
            - "MYSQL_DATABASE=wp"
            - "MYSQL_USER=wordpress"
            - "MYSQL_PASSWORD=wordpress"
            - "REPLICATION_MASTER=true"
            - "REPLICATION_USER=replica"
            - "REPLICATION_PASS=replica"
          external_links:
            - consul:consul
          labels:
            - "SERVICE_NAME=mysql-master"
            - "SERVICE_PORT=3306"
            - "SERVICE_TAGS=db"
          volumes:
            - '/mnt/storage/master:/var/lib/mysql'
    
    mysql-slave:
          image: mysql-slave
          ports:
            - "3307:3306"
          environment:
            - "REPLICATION_SLAVE=true"
            - "REPLICATION_USER=replica"
            - "REPLICATION_PASS=replica"
          external_links:
            - consul:consul
          labels:
            - "SERVICE_NAME=mysql-slave"
            - "SERVICE_PORT=3307"
            - "SERVICE_TAGS=db"
          volumes:
            - '/mnt/storage/slave:/var/lib/mysql'
    
    
    wordpress:
          image: fpm
          ports:
            - "9000:9000"
          environment:
            - "WORDPRESS_DB_NAME=wp"
            - "WORDPRESS_DB_USER=wordpress"
            - "WORDPRESS_DB_PASSWORD=wordpress"
          external_links:
            - consul:consul
          labels:
            - "SERVICE_NAME=php-fpm"
            - "SERVICE_PORT=9000"
            - "SERVICE_TAGS=worker"
          volumes:
            - '/mnt/storage/www:/var/www/html'
    



    Теперь осталось выполнить команду запуска нашего «докеризированного» приложения, откинуться на спинку кресла и любоваться результатом.
    docker-compose up
    


    Заключение



    Из достоинств



    — Распределённая архитектура приложения.
    Swarm прекрасно справляется с балансировкой нагрузки. Мы можем запускать сколько угодно копий приложения, пока на нодах есть ресурсы. И запускать «в один клик».

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

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

    — Запуск приложения в одну команду.
    Сценарий docker-compose позволяет нам развернуть всю инфраструктуру и связать приложения буквально в одно нажатие кнопки.

    Из недостатков



    Персистентные данные.
    Не раз уже говорилось о том, что у Docker не так всё гладко с stateful-сервисами. Мы пробовали flocker, но он показался очень сырым, плагин постоянно «отваливался» по непонятным причинам.
    Мы использовали для синхронизации персистентных данных сначала glusterfs, потом lsyncd. Glusterfs, вроде как, довольно неплохо справляется со своей задачей, но в продакшене мы его использовать пока не решались.

    Возможно, Вы знаете более изящный способ решения данной проблемы — было бы здорово его услышать.



    P. S.
    Данная статья нисколько не претендует на всеобъемлющее how-to, а лишь описывает базовые возможности использованных нами инструментов в конкретном юзкейсе.
    Если у вас есть более интересные решения/предложения по инструментами, решающим подобные задачи, буду рад увидеть их в комментариях.
    Southbridge
    272,00
    Обеспечиваем стабильную работу серверов
    Поделиться публикацией

    Похожие публикации

    Комментарии 63

      0
      Не раз уже говорилось о том, что у Docker не так всё гладко с stateful-сервисами. Мы пробовали flocker, но он показался очень сырым, плагин постоянно «отваливался» по непонятным причинам.
      Так же слышал про эту проблему. Это очень серьезная проблема, если вдруг у базы данных оторвется диск.
      Тут гарантирована потеря данных и простой сервиса. То есть решение не подходит для прода?

      Как быть? Поднимать базу отдельно в виртуалке вне докера?
        0
        Или поднимать такие вещи как Ceph, Amazon S3 или подобные решения. Мы, к сожалению, не использовали их.
        Здесь немного написано о текущих поддерживаемых платформах https://docs.docker.com/registry/storagedrivers/
          0
          Это хорошо, но имхо у базы все равно должен быть локальный диск. Иначе с какой скоростью она будет работать?
          И как обеспечить, что это хранилище так же не оторвется?
          0
          Я, например не сторонник связывания контейнеров между собой, поэтому постгрес/мускуль у меня живут отдельно, и докеризированный apache+php с вордпрессами у меня бегают в SQL по IP. да, при рестарте ip меняется, поэтому у меня "обвязка" которая при запуске контейнера обновляет его IP в локальной DNS зоне
            0
            А где мускуль данные хранит? Куда пых закачивает картинки?
            Вам же все равно надо как-то пробросить локальный каталог в контейнер.
              0
              Да, Вы правы. есть проброс каталога с ноды в /var/www/html контейнера.
              А SQL вертится отдельно в полноценном окружении, с бекапами мониторингом и все такое, чего нельзя сделать в docker'ной виртуалке
                0
                Ясно спасибо.
              0
              Посмотрите в сторону weave или того же consul'а — у них есть встроенные dns-сервера, которые получают данные о сервисах не от самих сервисов, а слушая docker-сокет, что избавляет от необходимости самому делать обновление DNS и обработку, например, падения контейнера.

              У себя я тоже полностью отказался от связывания и сейчас использую weave — доволен им: во-первых, есть внутренняя сеть контейнеров, независимая от внешней топологии и позволяющая связывать разнородные сети, в том числе, разные датацентры; во-вторых, есть надежное автообновление DNS при старте-остановке контейнера, которое позволяет реализовать красивую автобалансировку.
                0
                У меня заморочка в том, что образ apache+php один, а вот тонна контейнеров с примонтированными к ноде /var/www/html и контент сайтов у всех разный, как и "доменные" имена. поэтому я пока не могу понять как мне поможет консул. где один контейнер — один сайт
                (Да, я про шаредхостинг)
                  0
                  Нууу… В принципе, если все настроено и работает, то шило на мыло менять смысла действительно нет :)

                  Но если этот шаредхостинг надо будет активно масштабироваться (новые домены подключать и отключать), то может и помочь в том плане, что единожды настроенный, он будет отслеживать создание-удаление контейнеров и под них создавать-удалять vhost'ы на фронтэнде/прокси (домены можно будет прописывать или в переменных окружения для каждого контейнера из той тонны сайтов, или в лейблах для них же).
              0
              Тоже думал над решением этой проблемы, пока что в качестве "универсального и надежного" решения вижу только репликацию средствами самой БД, при которой если одна нода падает, перевыбирается новый мастер и все продолжается дальше.
                +1
                Радует, что мы не одни такие.
                  0
                  Кстати, чуть выше el777 написал про пыха, который куда-то должен закачивать картинки, и вот в этом как раз у нас и получилась проблема на одном из сервисов — классический портал, на котором медиа-файлы закачиваются на себя же. И если с базами есть репликация на уровне приложения, то тут несколько печальней — свой велосипед пилить не хочется, glusterfs с бухты-барахты в продашкшен пихать тоже не очень, а из остального пока что вижу только либо что-то вроде S3, либо GridFS от MongoDB.
                    0
                    На самом деле я глубоко "за" хранение картинок во внешнем сервисе — у нас сделан свой сторадж для этого.
                    Но вот хранение базы на удаленном диске или ее обновление по webdav — представляю с трудом.
                      0
                      На самом деле, я тоже за, только в моём случае полпроекта придётся переписать для использования внешнего хранилища ;)
                      0
                      Я выше и описал же, что да, проброс каталогов есть. Правда, сейчас проблема выбора кластерной фс.
                        0
                        Вот и я о ней же :)
                0
                Почему swarm, а не nomad?

                PS Поправте по мелочи форматирование статьи :) Например баш портянка.
                  0
                  Спасибо, поправил.
                  Так стояла задача от клиента. Лично я наткнулся на него на сайте Docker и привлекло словосочетание native clustering system. Но nomad хотелось бы тоже попробовать.
                    0
                    Мы сейчас больше смотрим в сторону nomad, особенно потому, что он qemu умеет стартовать. По мелочи нам это будет нужно.
                    +1
                    У swarm'а есть преимущество в том, что он работает прозрачно (хотя и не совсем, там есть проблемы с томами и портами при остановке контейнера), т.е. можно при помощи стандартного docker-клиента управлять всем кластером.

                    Кроме того, он может работать полностью как stateless-сервис, т.е. надо — запустил менеджер на какой-то ноде, поуправлял остальными, остановил, пошел кофе пить, потом пришел, запустил на другой ноде, поуправлял, остановил, опять пошел кофе пить :)

                    Да и проще с него начинать знакомиться с кластерами на контейнерах.

                    А nomad кажется интересной штукой, учитывая то, что он делается теми же, кем и consul, там должна быть хорошая интеграция. Надо будет как-нибудь попробовать.
                      +1
                      На сколько я помню по документации, они почти идентичны по функционалу. В какой-то мере докерцы делали swarm с оглядкой на nomad. (не уверен, но где-то пробегало)
                    +3
                    Простите, но картинки размером по высоте на половину\полный экран не располагают к чтению, мягко говоря. Это ведь не вконтакте.
                      +1
                      Вот все пишут о настройке, но мало кто пишет об обслуживании.

                      Вот у меня до сих пор много вопросов по последующему развертыванию релизов и zero-downtime, откату, отслеживанию обновления вышестоящих образов, в том числе ОС, обновления всех нижестоящих, по этому событию
                        0
                        Тоже не очень понятно как делать стейджинг или хотя бы просто обновление приложения.
                        Рядом разворачивать второе? Потом переключать?

                        Как я понимаю, docker — это реализация концепции Immutable Server, возведенная в абсолют. То есть мы никогда не делаем обновления чего бы то ни было — мы просто разворачиваем с нуля новое. Ок, имеет право на жизнь. Но как сделать "безшовный переход" от старого к новому?
                          0
                          Всё что я вычитал — нужно ставить балансировщик сверху, разворачить рядом новое, и тут опять вопрос: как это сделать на плавно n-количестве контейнеров.
                            0
                            Сверху == вне докера?
                            Как он тогда будет угадывать куда направлять запрос, если внутри докера все будет динамически "плясать" — сейчас на одном порту, завтра на другом?
                              0
                              внутри докера, nginx или ha-proxy. Вопрос связей решается как статье описано (т.е по сути добавление контейнера как n+1)
                              0
                              у меня в проекте шаредхостинга логика достаточно простая:
                              1+n нод с контейнерами образа (apache + php). на ноде nginx, между нодами ospf для серой сети с айпишниками контейнеров.
                              Локальная dns зона, куда при старте контейнера обновляется ip адрес, потом nginx reload
                              в случае например обновить образ на ноде — ну ок, запускаем эти же контейнеры на другой ноде, первую выводим из обслуживания, обновляем образ, пересоздаем контейнеры, запускаем, работаем.

                              Пока не выбрал только кластерную fs для репликации клиентского контента
                                0
                                первую выводим из обслуживания, обновляем образ, пересоздаем контейнеры, запускаем, работаем.

                                руками всё это? а если контейнеров много? Ведь нужно учитывать ревизию + возможность отката
                                  +1
                                  Ну дело Ваше — хотите руками, хотите — автоматизируйте.
                                  Я вот насмотрелся на все эти панели хостинга на PHP, что платные, что бесплатные, и делаю теперь сам, на рельсах, кластерный шарехостинг, ибо надоело — в платных "да, мы знаем про этот баг, покупайте новую версию за стотыщ денег, в которой он пофикшен", в бесплатных "ой, тут баг висит второй год, и черт с ним, закрыли"
                                    0
                                    Я уже просто не в первый раз рыскаю в инете в поисках адекватного ПО для деплоя контейнеров (аналогичного mina, capistrano). Возможно нужен какой-то другой подход.
                                      0
                                      ansible?
                                        0
                                        мне нужно, чтобы ci это делал, а не cm.
                                          0
                                          И в чем проблема?
                                            0
                                            Мне кажется смешивать puppet и ansible будет излишне.
                                              0
                                              Не видел (или не заметил), что где-то есть папет.
                                              В моем случае (мы выпиливаем puppet), смесь вполне работает. В целом, и на puppet можно все забацать.
                                              0
                                              на одном из прошлых проектов ci после тегирования коммита как релиз сама ансиблом пинала балансер, выводила ноду из экспуатации, загружала туда джанговый проект, запускала чо нужно и так дальше, по всем нодам.
                                              не вижу принципиальной разницы между деплоем джанго проекта и контейнера
                                                0
                                                Кстати да.
                                                  0
                                                  С балансировкой — хорошая мысль. А если это swarm? Поидее нужно через docker API подключаться и тормозить по очереди… Вот такое ПО я ищу.
                                                    0
                                                    Чем вам perl/bash/curl не угодили? меня они вполне устроили.
                                                    В swarm помнится те же docker команды умеет, только играет роль прослойки.
                                                      0
                                                      Тем что на это нужно время, ресурсы и тд. Смотрел видео от badoo — они писали свою систему.
                                                        0
                                                        Ну а я написал свою. Теперь вот, дело за вебмордой, это гораздо сложнее, чем нарисовать всю техническую часть обслуживающих скриптов и обвязок
                                                      0
                                                      Стоп, или я не могу уловить вашу боль, или Consul template. В моем случае ansible+consul+consul template.
                                                        0
                                                        Это не то. Они используются, и для управления балансировщиком ок
                                                        0
                                                        Боль в том, что допустим у меня в кластере swarm 20+ нод. Мне нужно подключиться по docker API, погасить по очереди нужные контейнеры, скачать правильные новые(по номеру релиза), и мягко пиная балансировщик, обновлять.
                                                          0
                                                          Опять же, я не вижу проблемы, особенно при грамотно организованной базе consul. Имена контейнеров я из нее и беру.
                                                            0
                                                            Если воспользоваться советом — и писать своё — то вопросов нет.
                                                              0
                                                              а capistrano, ты сам логику не пишешь разве?
                                                              У меня минимум каких-либо вызовов, и все завернуто в ansible. Смысла искать что-либо нет. Еще один лишний продукт, который не будет решать ничего, что нельзя решить текущими.
                                                                0
                                                                привязка к конкретным машинам
                                                                  0
                                                                  consul
                                                                    0
                                                                    Да, согласен, но все же это будет запуск контейнера на машине, в не в swarm.
                                                                      0
                                                                      если я верно помню swarm (у нас сейчас ручная балансировка и внедряем nomad), то там главное, чтобы нода была в swarm, а дальше ты работаешь так же командой docker.
                                                                        0
                                                                        На текущий момент, если ничего не найду — буду писать плагин к mina для работы через docker api. Благо есть gem. Вот ещё нашел — https://github.com/newrelic/centurion, но пока не присматривался
                                                                          0
                                                                          Ну такое для меня лишнее.
                                                                          сие:

                                                                          Назначаем демону ноды метку:
                                                                          `docker daemon --label com.example.storage=«ssd»`

                                                                          Запускаем PostgreSQL с фильтром у указанной метке:
                                                                          `docker run -d -e constraint:com.example.storage=«ssd» postgres`

                                                                          в паре с указанным выше решает мои проблемы.
                                                                            0
                                                                            И остаётся вопрос актуализации образов. Нашел, что раньше можно было ставить свои hook на базовые образы(из library). Сейчас нельзя. Ну дальше по цепочке обновлять все образы зависимые.
                                                                              0
                                                                              В моем случае тут все решает Bamboo (если угодно Jenkins или teamcity).
                                                                                0
                                                                                хуки на дочерние образы? Но если например обновился debian образ — тут не отследишь автоматически.
                                                                                  0
                                                                                  А, я понял вас. Вот она сила привычки. :)

                                                                                  У меня следующая схема (в ci)
                                                                                  job1 — через packer собираю базовый образ (в моем случае ubuntu 14.04)
                                                                                  Далее триггеры CI пересобирают базовые сервисные образы.
                                                                                  Если деплой уже был, то новые образы выливаются на следующий день с основным деплоем (иногда проводим синхронизацию базовых образов на целевые машины). Если обновляем что-то блокерное, то идем по регламенту штатного деплоя.
                                                                                    0
                                                                                    То есть я независим от Docker Hub. У меня заведена политика минимальной зависимости от внешних источников софта, в том числе и по причине отслеживания версий.
                                      0
                                      Вот тут в комментариях красиво развернули тему обновления контейнеров https://habrahabr.ru/post/277699/
                                      +1
                                      А как мониторите это хозяйство?
                                      (метрики, распределение контейнеров)

                                      Как определяете, что надо произвести ребалансировку?
                                        0
                                        Пока все спецы по докеру не разбежались, поспрашиваю вас.

                                        1) docker daemon запускается с указанием локального consul. Если контейнер с консулом умирает по любой причине, то наша нода теряется для кластера? Думал над тем, чтобы прятать консулы за балансером.

                                        2) Как правильно масштабировать приложение? Запустили N контейнеров, а перед ними haproxy/nginx и consul-template. Но теперь у нас нод больше чем одна и что дальше? Получается перед балансировщиком ставим ещё один балансер?

                                        3) Оверлейная сеть. Запускать все проекты в одной сети кажется не очень безопасным. Пока на каждый проект создаю свою подсеть. Здраво ли?

                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                        Самое читаемое