В этом посте я расскажу как мы, в TheQuestion, осуществили нашу давнюю мечту — отдельные, автоматически разворачиваемые development среды для каждой отдельной задачи.
С самого начала наша разработка строилась таким образом:
- Есть
master
ветка на GitHub, для которой настроена continuous integration — полная автоматизация деплоя на единственный тестовый сервер, архитектура которого максимально повторяет production. - Каждая новая задача ведется в fork-ветке разработчика, затем открывается пул реквест на
master
, который в итоге туда мержится.
Стоит сказать, что CI для master
ветки устроен вполне обычным способом:
- Пуш на гитхаб
- TeamCity видит новый коммит и делает
make
- Прогоняются автоматические тесты
- Собираются Docker контейнеры
- Ansible деплоит контейнеры
Эту последовательность и инструменты хотелось сохранить, чтобы не менять многое.
Очевидным недостатком одного dev'а является то, что на нем можно смотреть одновременно только одну ветку, незаконченные задачи мешают друг другу и приходится решать постоянные конфликты. Наша же цель заключалась в следующем: как только создается новая ветка на GitHub, создается отдельный dev для нее.
С первого взгляда задача не сложная — мы смотрим в API нашей облачной платформы и перед тем, как перый коммит в новой ветке начнет свой путь, создаваем для этой ветки отдельный сервер — все просто, развертка на отдельно взятой машине уже есть, спасибо Ansible!
Но тут есть одна существенная проблема: наша база данных. Полная ее развертка из сжатого dump (надо же еще скачать) на скромной машине занимает порядка двух часов. Можно конечно деплоить это все на более производительные машины или просто подождать, но мучится с API облака (при том, что при переезде на другое пришлось бы все переписывать) и платить лишнюю копеечку за каждую новую машину не хотелось. Так что для нашего решения используется одна средняковая машина.
TeamCity
Это замечательный инструмент, который почти не нужно настраивать. Единственное, что от него требуется — это рассказать скриптам, с какой веткой он работает.
Так что изменение единственного Build Step: command line
из
cd clusters/dev
make
превратилось в
export branch_name=%teamcity.build.branch%
cd clusters/dev
make
Docker
При одном деве каждая часть инфраструктуры, будь то часть приложения приложения, Sphinx, Redis, Nginx или PostgreSQL запускались внутри отдельного контейнера. Которые запускались с указанием --network-mode=host
, то есть каждый ip:port
контейнера совпадал с localhost:port
хост-машины.
Как вы понимаете, для нескольких девов это не прокатит, во-первых контейнеры должны общаться только с контейнерами одной ветки, во-вторых, nginx
должен знать внутренние IP каждого нужного ему контейнера.
Тут на помощь приходит Docker network
и запуск контейнеров превращается из
docker run /path/to/Dockerfile
в
docker network create ${branch_name} --opt com.docker.network.bridge.name=${branch_name}
docker run --network=${branch_name} -e branch_name=${branch_name} /path/to/Dockerfile
Это дает нам:
- название docker сети совпадает с названием ветки
- название интерфейса сети совпадает с названием ветки
- контейнеры каждой ветки находятся в одной docker сети, что позволяет им общаться по их именам (docker создает DNS-записи внутри каждой своей
bridge
сети) - внутри контейнеров создается переменная окружения с названием ветки, необходимая для генерации различных конфигов
PostgreSQL
Его мы запускаем в контейнере с --network=host
, как раньше, чтобы СУБД была одна, но для каждый ветки — свой юзер и своя база.
Задача быстрого разворачивания новой базы отлично решается шаблонами:
CREATE DATABASE db_name TEMPLATE template_name
Плюс, каждый день хотелось бы иметь свежую копию базы с прода, чтобы при создании ветки, основываться на ней (тоже протекает в отдельном контейнере с --network=host
)
Для этого создаем две базы. Каждую ночь тратим два часа на разворачивания свежего дампа в одну:
pg_restore -v -Fc -c -d template_new dump_today.dump
и если успешно:
DROP template_today;
CREATE DATABASE template_today TEMPLATE template_new;
По итогу имеем свежий шаблон каждое утро, который останется, даже если очередной дамп придет битым и развернется неуспешно.
При создании новой ветки создаем базу из шаблона
CREATE USER db_${branch_name};
CREATE DATABASE db_${branch_name} OWNER db_${branch_name} TEMPLATE template_today;
Таким образом, на создание отдельной базы под ветку уходит 20 минут, а не 2 часа, а подключение к ней изнутри docker-контейнеров осуществляется по eth0
инетрфейсу, который всегда указывает на IP хост-машины.
nginx
Его мы так же установим на хост-машине, а конфигурацию будем собирать с помощью docker inspect
— эта команда дает полную информацию о контейнерах, из которой нам нужно одно: IP адрес, который подставим в шаблон конфигурации.
А благодаря тому, что имя интерфейса сети совпадает с названием ветки, может генерировать одним скриптом конфиги для всех девов сразу:
for network in $(ip -o -4 a s | awk '{ print $2 }' | cut -d/ -f1); do
if [ "${network}" == "eth0" ] || [ "${network}" == "lo" ] || [ "${network}" == "docker0" ]; then
continue
fi
IP=$(docker inspect -f "{{.NetworkSettings.Networks.${network}.IPAddress}}" ${container_name})
sed -i "s/{{ ip }}/${IP}/g" ${nginx_conf_path}
sed -i "s/{{ branch_name }}/${network}.site.url/g" ${nginx_conf_path}
done
Удаление веток
Из-за недолгой жизни каждой ветки, кроме master
, возникает необходимость переодически удалять все, что относится к ветке — конфиг nginx, контейнеры, базу.
К сожалению, я не смог найти, как заставить TeamCity рассказать, что ветка удалена, поэтому пришлось исхитряться.
При деплое очередной ветки, на машине вызывается создается файл с ее именем:
touch /branches/${branch_name}
Это позволяет запоминать не только все ветки, которые у нас есть, но и их время последнего изменения (оно совпадает со временем изменения файла). Очень полезно, чтобы удалять ветку не сразу, а через неделю после того как она перестает использоваться. Выглядит он примерно следующим образом:
#!/usr/bin/env bash
MAX_BRANCH_AGE=7
branches_to_delete=()
for branch in $(find /branches -maxdepth 1 -mtime +${MAX_BRANCH_AGE}); do
branch=$(basename ${branch})
if [ ${branch} == "master" ]; then
continue
fi
branches_to_delete+=(${branch})
done
dbs=()
for db in $(docker exec -it postgresql gosu postgres psql -c "select datname from pg_database" | grep db_ | cut -d'_' -f 2); do
dbs+=(${db})
done
for branch in ${branches_to_delete[@]}; do
for db in ${dbs[@]}; do
if [ ${branch} != ${db} ]; then
continue
fi
# branch file
rm /branches/${branch}
# nginx
rm /etc/nginx/sites-enabled/${branch}
# containers
docker rm -f $(docker ps -a | grep ${branch}- | awk '{ print $1 }')
# db
docker exec -i postgresql gosu postgres psql <<-EOSQL
DROP USER db_${branch};
DROP DATABASE db_${branch};
EOSQL
done
done
service nginx reload
Несколько подводных камней
Как только все заработало и помержено в master
— он не собрался. Оказывается, слово master
ключевое для утилиты iproute2
, так что вместе нее для определения IP контейнеров, стали использовать ifconfig
было:
ip -o -4 a s ${branch_name} | awk '{ print $2 }' | cut -d/ -f1
стало:
ifconfig ${branch_name} | grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'
Как только была создана ветка thq-1308
(по номеру задачи из Jira
) — она не собралась. А все из-за тире. Оно мешается в нескольких местах: PostgreSQL и шаблон вывода Docker Inspect
В итоге, узнаем IP хоста:
docker inspect -f "{{.NetworkSettings.IPAddress}}" ${network}-theq
Изменяем владельцев всех таблиц новой базы:
tables=`gosu postgres psql -h ${DB_HOST} -qAt -c "SELECT tablename FROM pg_tables WHERE schemaname = 'public';" "${DB_NAME}"`
for tbl in $tables ; do
gosu postgres psql -h ${DB_HOST} -d "${DB_NAME}" <<-EOSQL
ALTER TABLE $tbl OWNER TO "${DB_USER}";
EOSQL
done
В общем-то, это все. Не приводил полных команд, скриптов (разве что кроме последнего), ролей ansible — там ничего особенного, но надеюсь сути не упустил. На все вопросы готов ответить в комментариях.
Комментарии (12)
tipugin
16.03.2017 22:45А связку registrator/consul/consul-template не рассматривали для конфигов nginx?
farcaller
16.03.2017 23:24У меня так. Consul начинает прилично подтупливать и, иногда, вообще не обновляет сервисы. Вариант кранить в консульном KV настройки vhost'ов для consul-template вообще нифига не масштабируется. Смотрю в сторону etcd, или уже сразу k8s.
ngalayko
17.03.2017 00:45да их и jinja2 которая в ansible встроена можно было бы верстать, но так как никакой сложной логики нет, подставляй да и все, то просто sed ограничился
amberovsky
17.03.2017 00:12- Раз уж используете докер — то делайте сборку проекта в докере.
- Почему не подошёл docker-compose? Точно такое же изолированное окружение со своей сетью и dns.
- Прописываете * в днс на сервере и делаете поддомен на каждый новый бранч. Вот это поможет не плясать с бубном и не теребить docker inspect
Если ваше использование teamcity ограничивается вышенаписанным, то в конце заворачиваете всё в ansible и, в общем-то, teamcity становится не особо нужным.ngalayko
17.03.2017 00:28Возможно из приведенных кусков кода не ясно, но все задачи makefile'ов выглядят примерно так, все в своих контейнерах
docker build ... docker run ... ansible-playbook ...
Docker-compose делает тоже самое, что и настроенные ansible роли для контейнеров ( https://docs.ansible.com/ansible/docker_container_module.html ), а дергать ансиблом docker-compose по сути тоже самое, но будет один большой файл, а не полностью разделенные (в том числе по файлам, для удобной навигации) файлики ролей
За репозиторий спасибо, гляну! Выглядит отличным решением, плюс тот же докер в итоге станет
А тимсити нужен, по большому счету, чтобы при изменениях в git, запускать ансибл (разве сам ансибл можно настроить так?), ну и всяческие задачи по расписанию
amberovsky
17.03.2017 01:371. Я стараюсь разграничивать роли и обязанности, чего и вам советую. Если у меня есть плейбуки, в которых прописана логика работы с моим докер-окружением, значит у меня билд имаджа тоже в плейбуке. (см ниже про teamcity)
2.Docker-compose делает тоже самое, что и настроенные ansible роли для контейнеров (
Таки нет. Этот модуль — обёртка над docker api. docker-compose в общем-то, простенькая утилита для удобного развёртывания/прибивания докер-окружения, которую, кстати, можно использовать в связке со swarm. Из приятных бонусов, как я уже упомянул — dns и изолированная сеть, а ещё не надо думать про имена хостов и контейнеров.
3. Если вынести весь management в ansible (чего я советую), то teamcity, как дорогой enterprise-продукт, становится избыточным. Я, честно говоря, не имею опыта использования teamcity как scheduler, возможно это как-то оправдано, но я бы порекомендовал использовать что-то другое, в зависимости от проекта.
M_Muzafarov
17.03.2017 09:26Вместо touch /branch_name можно обращаться напрямую к Git, например, через Github API, или через git branch (в зависимости от глубины репозитория при клонировании). Не даст времени последнего изменения, но, по крайней мере, избавит от зависимости от файлов и одной машины.
А для совместимости со всякими базами и прочим я обычно использую что-нибудь вроде
${BRANCH_NAME//-/_}
grossws
17.03.2017 13:35-1можно обращаться напрямую к Git, например, через Github API
И мой локальный, и удаленный git-репозитории отказались отвечать через GitHub API. ЧЯДНТ?
servarius
А не пробовали смотреть в сторону кластеризации/оркестрации типа kubernetes и openshift? Не нужно было бы заморачиваться с вводом новых ВМ, сетью на уровне docker. Для каждого дева свой namespace, поддомен и т.д. да и деплой из Гита есть из коробки.
ngalayko
Пробовал, как раз
Первой мыслью были кубернейтес или докер сворм. Но почитав, понял, что они именно для многих хостов, плюс переписывать пришлось пришлось бы много всего, настраивать с нуля…
Хотелось максимально сохранить налаженную схему и сохранить окружение как на проде
Кластеризация это круто, конечно, и в планах перейти на них есть, но не в приоритете пока что
servarius
Еще дополню, раз в планах есть — fabric8. Деплоится на OpenShift, kubernetes и miniShift. Разворачиваются нэймспейсы с набором утилит cd-pipeline, сразу три окружения типа test-staging-prod.