Инструменты, которые мы будем использовать
Docker
Docker — простая и элегантная библиотека для создания легковесных изолированных друг от друга виртуальных контейнеров, в которых можно исполнять любой код. Совершенно не требователен к ресурсам, минимальный overhead.
Собрав контейнер один раз, его можно многократно использовать.
Простой пример — это БД Redis. Если нам необходимо несколько серверов Redis на одном компьютере, при обычном подходе нам придется изменять конфигурационные файлы в /etc/redis и менять файлы в /etc/init.d. Можно написать bash скрипт, но это не делает процесс легче.
В случае Docker, мы можем использовать следующую команду:
docker run -d --name test-redis-server dockerfile/redis
Эта команда скачает контейнер Redis из главного репозитория (index.docker.io), запустит его в фоновом режиме и присвоит только что созданному контейнеру имя test-redis-server.
Этот контейнер можно запустить потом командой:
docker start test-redis-server
SkyDNS
Совсем недавно разработчики Docker ввели инструмент --link при запуске контейнера для того, чтобы можно было связать несколько контейнеров посредством переменных окружения ENV. Например для связывания контейнера веб приложения с контейнерами Redis, Postgresql, Elasticsearch и т.д.
Это удобно, но нам необходимо следить за правильностью ENV и менять код при изменении условий запуска контейнеров и параметра --link.
Это называется Service Discovery, для него существует множество решений (более подробно про существующие решения вы можете прочитать в статье Open-Source Service Discovery).
Одно из таких решений — это SkyDNS. Небольшой локальный DNS и DNS proxy сервер, написанный на языке Go (важно заметить, что Go крайне эффективен в плане потребления ресурсов, примерно до 6MB памяти при запуске. Я проводил лично для себя несколько синтетических тестов, максимальное потребление памяти при 50 одновременных простых запросах было около 20MB).
SkyDNS позволяет с помощью простого API (более подробно: SkyDNS) добавлять DNS записи, после чего делать обычные запросы, которые возвращают SRV, A или AAAA записи.
Если запись не будет найдена, то SkyDNS отправит запрос на публичные dns сервера google (8.8.8.8/8.8.4.4).
В SkyDNS используется собственная интересная схема для домена:
<uuid>.<host>.<region>.<version>.<service>.<environment>.skydns.local
Если environment = production, а service = redis, то можно сделать запрос к redis.production.skydns.local, который вернет одну или несколько записей (по умолчанию, он возвращает A запись).
Skydock
Skydock — это небольшая программа, которая объединяет Docker и SkyDNS.
Skydock использует схему домена SkyDNS следующим образом:
<name>.<container-name>.<environment>.skydns.local
сontainer-name — это название образа Docker без репозитория (например crosbymichael/redis => redis или test/cool-api => cool-api). name — это название контейнера, присвоенное ему с помощью параметра --name.
Skydock автоматически определяет запущенные контейнеры и добавляет их в SkyDNS.
Docker присваивает отдельный IP адрес для каждого контейнера, следовательно если мы запустим контейнер следующим образом:
docker run -d --name redis-test-app dockerfile/redis
Мы сможем сделать запрос (позже будет описано почему мы делаем запрос на 172.17.42.1) и получим в ответ A запись, то есть IP адрес контейнера, в котором запущен Redis.
dig @172.17.42.1 redis-test-app.redis.dev.skydns.local
dev.skydns.local. 27 IN A 172.17.0.3
Порт Redis по умолчанию 6379, поэтому коде мы можем сделать нечто такое (пример на псевдоязыке):
redisConn = redis.Connect("redis-test-app.redis.dev.skydns.local:6379")
Установка
Мы не будем рассматривать установку Docker, она очень простая и детально описана тут: Start using Docker
Нам нужно запустить 2 контейнера — один с SkyDNS, другой с Skydock. В отличии от автора Skydock, я рекомендую запускать SkyDNS с дополнительным параметром -p 172.17.42.1:8080:8080 — это позволит вашим контейнерам использовать API SkyDNS напрямую для собственных нужд.
docker pull crosbymichael/skydns
docker run -d -p 172.17.42.1:53:53/udp -p 172.17.42.1:8080:8080 --name skydns crosbymichael/skydns -nameserver 8.8.8.8:53 -domain skydns.local
IP 172.17.42.1 — это мост docker0, используется Docker для конфигурирования собственной сети. Мы указали домен skydns.local, хотя можно сделать любой, на ваш выбор, для примера: docker, super.local. skydns.local по моему мнению удобнее и универсальнее, тем более используется по умолчанию в SkyDNS.
Далее запускаем Skydock:
docker pull crosbymichael/skydock
docker run -d -v /var/run/docker.sock:/docker.sock --name skydock -link skydns:skydns crosbymichael/skydock -ttl 30 -environment dev -s /docker.sock -domain skydns.local
Здесь нужно указать TTL (если он меньше — более динамические окружение, можно быстрее вносить больше изменений в архитектуру. Если больше — менее динамическое, более кешированное окружение) и environment — это может быть совершенно любая строка (dev, development, production, stage, qa).
Если все прошло успешно, то все необходимые компоненты запущены.
Единственное, что нужно сделать — это при запуске контейнера с вашим приложением, необходимо указать первичный DNS сервер:
docker run -d --dns 172.17.42.1 test/cool-api
Наш контейнер с cool-api будет доступен в локальном DNS по адресу: cool-api.dev.skydns.local — это выдаст список IP адресов всех контейнеров с названием cool-api. Это свойство мы и будем использовать для конфигурирования nginx.
Если при запуске контейнера указать --name api1, то он будет доступен по api1.cool-api.dev.skydns.local — именно только 1 контейнер с именем api1.
Внутри контейнера теперь можно указывать домен напрямую, так как он будет использовать локальный DNS: redis.dev.skydns.local — вернет А записи всех контейнеров с запущенным Redis. Естественно будет выбран только 1 адрес, к которому подключится клиент Redis.
nginx
Использую эту магию локального DNS, мы можем сделать например:
- Балансировку нагрузки — веб сервера или подключений к базе данных
- Простой роутинг запросов по критерию. Например создав образ test/cool-api-v1 — мы можем направлять запросы к API v1 на одни контейнеры (cool-api-v1.dev.skydns.local), а cool-api.dev.skydns.local использовать как последнюю версию. При этом с автоматической балансировкой
- Обновление кода на лету — что нам и нужно
Добавим в nginx вот такой конфигурационный файл:
server {
listen 80;
server_name super-cool-domain.com;
# говорим nginx использовать SkyDNS
resolver 172.17.42.1 valid=5s;
resolver_timeout 5s;
# Это нам необходимо сделать для того, чтобы nginx использовал локальный DNS. По-другому nginx не понимает
set $dns cool-api.production.skydns.local;
location /api {
proxy_pass http://$dns:8080;
}
# Чисто для примера
location / {
try_files $uri /index.html;
}
}
Мы просто указываем, чтобы nginx использовал локальный DNS сервер, далее указываем куда обращаться — переменная $dns и делаем старый добрый proxy_pass на тот порт, который использует ваше приложение.
Теперь, при запуске нового контейнера, Skydock добавит его в SkyDNS, nginx будет проксировать запросы к этому контейнеру. При остановке контейнера, Skydock удалит его адрес из SkyDNS.
Запустив таким образом 2-3 контейнера, мы можем балансировать нагрузку между ними.
Запустим несколько дополнительных контейнеров, nginx автоматически начнет проксировать запросы к ним. После этого, мы можем остановить работу старых — тем самым сделав обновление на лету.
Это не будет требовать переконфигурирования. Все работает по умолчанию. По сути все, что нужно будет сделать, чтобы изменить код — это:
# Скачиваем новую версию контейнера с обновленным кодом веб приложения
docker pull test/cool-api
# Сохраняем ID текущих контейнеров
OLDPORTS=( `docker ps | grep cool-api | awk '{print $1}'` )
# Запускаем новый контейнер
docker run -d test/cool-api
# Останавливаем старые
for i in ${OLDPORTS[@]}
do
echo "removing old container $i"
docker kill $i
done
Заключение
Автор Skydock хочет развить идею и сделать поддержку нескольких серверов в датацентре. На данный момент это не реализовано, хотя сам SkyDNS уже используется в production окружении на множестве серверов.
В статье так же не затронута тема репозиториев docker. Есть решение с открытым кодом, которое позволяет создавать приватное хранилище образов.
Решение почти не влияет на производительность, Docker и SkyDNS очень хорошо спроектированы и используют эффективный и быстрый язык Go.
На первый взгляд кажется, что это некоторое усложнение процесса, но в итоге получается очень гибкое решение, которое не нужно дополнительно конфигурировать после первоначальной настройки.