Наш досвід знайомства з 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",
"Порту": 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~вірного
 

 
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;
 
}
 
}
 



Тепер нам потрібно зібрати модифікований спосіб:
bocker 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 fact that
// 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;
} | посередник 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 -E '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
-- products or like mysql-fabric won't work
SET @@SESSION.SQL_LOG_BIN=0;
CHANGE TO MASTER 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
-- products or 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
-- products or 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 in f /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; 
 
} | посередник 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 -E '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
-- products or like mysql-fabric won't work
SET @@SESSION.SQL_LOG_BIN=0;
CHANGE TO MASTER 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
-- products or 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
-- products or 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 in f /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}}
 


Збираємо:

bocker 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, а лише описує базові можливості використаних нами інструментів в конкретному юзкейсе.
Якщо у вас є цікаві вирішення/пропозиції щодо інструментами, вирішальним подібні завдання, буду радий бачити їх у коментарях.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.