Сборка образа без обращения к базе данных

Всем привет.

У меня уже несколько лет работает довольно небольшой экземпляр Discourse (без каких-либо проблем): https://discuss.cubeisland.de/.
Я использовал стандартный процесс развёртывания на основе launcher на выделенной виртуальной машине (на собственном оборудовании в дата-центре). Единственное, что я менял за эти годы, — это миграция на внешнюю общую базу данных PostgreSQL.

Недавно я начал переносить приложения с выделенных виртуальных машин в Docker Swarm в качестве подготовительного шага к eventual миграции на кластер Kubernetes, в основном для экономии ресурсов и повышения «эластичности» некоторых частей инфраструктуры.

Сегодня я решил заняться этим небольшим экземпляром Discourse, так как он был одним из немногих оставшихся приложений на выделенных виртуальных машинах. «Он уже работает в Docker, насколько сложно будет развёрнуть его в Swarm?» — подумал я. И, судя по тому, что я прочитал, это действительно возможно. Я могу просто взять образ из текущего работающего экземпляра, загрузить его в наш внутренний реестр и запустить в Swarm, и всё будет работать отлично.

Я изучил файлы launcher, особенно шаблоны и примеры, и понял, что в таком развёртывании, вероятно, стоит вынести Redis отдельно. Возможно, я мог бы настроить задачу CI для сборки новых образов при добавлении плагинов или при желании обновить систему. Поэтому я клонировал репозиторий discourse_docker локально, скопировал своё существующее определение контейнера app.yml в клон и попытался выполнить ./launcher bootstrap app, чтобы собрать образ, который затем можно было бы загрузить в мой реестр, не разворачивая его сразу.

К моему удивлению, скрипт попытался подключиться к «продакшн»-серверу PostgreSQL для миграции базы данных, к счастью, у него не было доступа к нему с моей локальной рабочей станции.

Я поискал информацию здесь и, похоже, именно так всё и работает, что заставляет меня задуматься:

  1. Как собрать контейнер для нового экземпляра, если у меня ещё нет базы данных? Нужно ли сначала настроить продакшн-базу данных, прежде чем собирать образ?
  2. Предполагаю, что db:migrate выполняется только один раз. Значит, если у меня есть несколько похожих экземпляров (например, prod и test), мне нужно будет обновить один из них, чтобы собрать новый образ, но тогда я не смогу использовать тот же образ для второго экземпляра, даже если образы будут идентичными.
  3. Как собирать образы для экземпляров, если сервер базы данных недоступен с системы, на которой собирается образ (что, вероятно, не так уж редко встречается)?

Прочитав несколько постов (очевидно, включая этот), я прекрасно понимаю причины текущего процесса сборки и вижу его ценность для упомянутых 99% людей, которые эпизодически разворачивают Discourse на стандартных «полноценных» виртуальных машинах. Я также привык к моделям «всё в одном» в контейнерах и не против них. В конце концов, ключевая ценность Docker заключается в том, что поставщик программного обеспечения может заранее оптимизировать конфигурации и упаковать их в воспроизводимую среду выполнения, избавляя операционную команду от необходимости обладать специфическими знаниями о приложении. Поэтому я полностью поддерживаю использование предоставленных вами инструментов. Зачем мне ожидать, что кто-то другой создаст лучшие контейнеры, чем сам поставщик ПО? Зачем разделять nginx и Ruby-приложение, если от этого нет никакой выгоды, только ради более «чистой» развёртки (что бы это ни значило)?

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

Первым примером, который приходит на ум, является GitLab. Хотя сейчас они предоставляют красивый Helm-чарт для полностью декомпозированной развёртки в Kubernetes «как должно быть», я предполагаю (без изучения статистики), что аналогичные 99% их развёрток малого и среднего размера используют Docker-образ GitLab Omnibus (или пакет ОС, что практически то же самое). У них есть аналогичный процесс инициализации, основанный на Chef внутри контейнера, который выполняется при каждом запуске и выполняет обычные действия, такие как миграции базы данных и компиляция ассетов.

Да, запуск GitLab может занимать несколько минут из-за этого, но в развёртках, с которыми я сталкивался (в том числе в крупных компаниях), это никогда не было проблемой. Особенно с современными системами оркестрации, такими как Docker Swarm и Kubernetes, которые могут выполнять rolling-обновления: старый экземпляр отключается только после того, как новый запущен и успешно прошёл проверки работоспособности и готовности. Поэтому длительный процесс развёртывания может не стать проблемой. Но даже без сложных rolling-обновлений, которые могут работать, а могут и нет, во многих ситуациях можно обойтись значительным временем простоя.

Итак: возможно ли настроить launcher так, чтобы он пропускал операции, зависящие от базы данных, во время сборки образа, а выполнял их при запуске контейнера?

Я готов потратить на это своё время, но по вечерам оно ограничено, поэтому любые подсказки будут очень кстати.

Также я открыт к совершенно другим подходам, если вы считаете, что мой вариант глупый или вообще невозможен.

Спасибо за любую обратную связь!

Я хотел сделать то же самое, что и вы: мы запускаем Discourse на Amazon ECS, поэтому нам нужно было иметь возможность собирать только веб-образ и загружать его в реестр. Мне не хотелось взламывать процесс сборки Discourse, так как мы хотим максимально приближаться к официально поддерживаемой установке.

Вместо этого мы используем обычный скрипт launcher для создания настройки из двух контейнеров на локальной машине, но игнорируем контейнер данных и загружаем веб-контейнер в реестр. Во время выполнения мы переопределяем детали подключения к Postgres и Redis через переменные окружения.

Развёртывание нового образа состоит из трёх шагов:

  1. Выполните безопасные предварительные миграции. Запустите в ECS эту команду (с новым образом):

     SKIP_POST_DEPLOYMENT_MIGRATIONS=1 rake db:migrate
    
  2. Разверните новый образ. Обновите сервис ECS.

  3. Выполните пост-миграции. Запустите в ECS эту команду:

     SKIP_POST_DEPLOYMENT_MIGRATIONS=0 rake db:migrate
    

Запуск локального контейнера данных во время сборки образа, вероятно, расточителен, но это позволяет использовать стандартный файл web.template.yml, не беспокоясь о том, какие его части пытаются подключиться к базе данных или Redis.

Спасибо за это! Я тоже подумал, что можно просто запустить PostgreSQL во время сборки образа и удалить его после завершения сборки самого образа.

Наконец-то я нашел время, чтобы это реализовать!

Я настроил сборку образа с помощью пайплайна GitLab CI, который запускает PostgreSQL и Redis как сервисы во время сборки, а затем удаляет их:

Теперь осталось только автоматизировать развертывание с миграциями базы данных.

Эта штука работает уже больше года, и мы не трогали её ни разу, даже для релиза 2.8.

Я перенес сборку образа на GitHub: GitHub - pschichtel/discourse-docker: A reusable Discourse container built using the launcher tool · GitHub.

Образ опубликован в pschichtel/discourse:stable-web_only.

Кажется, это наконец-то сломалось. При обновлении с версии 3.0.6 до 3.1.0 миграции базы данных не выполнялись. Однако запуск финальной команды bundle exec rake db:migrate внутри работающего контейнера сработал, но только после перезапуска контейнера.

Вам потребуется выполнить миграцию повторно, когда новый образ будет запущен без установки этой переменной окружения. Существует задача Rake, которая это делает, но я не могу вспомнить её название или найти с телефона. Что-то вроде ensure_post_migrations.

На всякий случай отмечу, что я не заметил никаких сбоев. Я в основном слежу за веткой бета-релизов и, насколько я могу судить, миграции успешно выполнялись на каждом этапе серии 3.1.0.beta…

Я нашёл db:ensure_post_migrations через rake -AT.

В чём разница между db:migrate с SKIP_POST_DEPLOYMENT_MIGRATIONS=0 и db:ensure_post_migrations?

Хорошо, посмотрев на код, я понял, что делает db:ensure_post_migrations. Его следует запускать в том же процессе Rake перед db:migrate, чтобы гарантировать, что переменная SKIP_POST_DEPLOYMENT_MIGRATIONS установлена в 0. Мой скрипт уже обеспечивает это:

файл .gitlab-ci.yml:

./migrate.sh pre || echo "Redis не запущен во время предварительных миграций, пропускаем..."
docker stack deploy --prune --resolve-image always -c "$STACK.yml" "$STACK"
./docker-stack-wait.sh -t 180 "$STACK"
./migrate.sh post

файл migrate.sh:

#!/usr/bin/env sh

if [ "$(docker ps -q --filter "label=com.docker.stack.namespace=${STACK}" --filter "label=com.docker.swarm.service.name=${STACK}_${DISCOURSE_REDIS_HOST}" | wc -l)" = "0" ]
then
    echo "Контейнер Redis не найден, невозможно выполнить миграции!"
    exit 1
fi

if [ "$1" = "pre" ]
then
    skip_post=1
else
    skip_post=0
fi


docker run \
    --rm \
    --name "discourse-migration-${DISCOURSE_DB_HOST}-${DISCOURSE_DB_NAME}" \
    --network "${STACK}_discourse" \
    --workdir /var/www/discourse \
    -u discourse \
    -e SKIP_POST_DEPLOYMENT_MIGRATIONS="$skip_post" \
    -e LANG="${LANG}" \
    -e DISCOURSE_DEFAULT_LOCALE="${DISCOURSE_DEFAULT_LOCALE}" \
    -e DISCOURSE_HOSTNAME="${DISCOURSE_HOSTNAME}" \
    -e DISCOURSE_DEVELOPER_EMAILS="${DISCOURSE_DEVELOPER_EMAILS}" \
    -e DISCOURSE_SMTP_ADDRESS="${DISCOURSE_SMTP_ADDRESS}" \
    -e DISCOURSE_SMTP_PORT="${DISCOURSE_SMTP_PORT}" \
    -e DISCOURSE_DB_USERNAME="${DISCOURSE_DB_USERNAME}" \
    -e DISCOURSE_DB_PASSWORD="${DISCOURSE_DB_PASSWORD}" \
    -e DISCOURSE_DB_HOST="${DISCOURSE_DB_HOST}" \
    -e DISCOURSE_DB_NAME="${DISCOURSE_DB_NAME}" \
    -e DISCOURSE_REDIS_HOST="${DISCOURSE_REDIS_HOST}" \
    "$DISCOURSE_IMAGE" \
    bundle exec rake db:migrate

Скрипт выполняет db:migrate с параметром SKIP_POST_DEPLOYMENT_MIGRATIONS=1 в новом образе Docker Swarm, пока Discourse всё ещё работает в старой версии. Затем новый образ разворачивается в Swarm, и скрипт ожидает завершения сходимости. В конце снова выполняется db:migrate, но уже с SKIP_POST_DEPLOYMENT_MIGRATIONS=0.

Это надёжно работало для каждой версии уже более двух лет. Учитывая, что у вас это тоже сработало, @simonk, вы делали что-то принципиально иное по сравнению с моим скриптом?

Нет, я всё ещё следую тому же процессу, который описал здесь, и, насколько я могу судить, он примерно такой же, как у тебя. Я использую просто rake db:migrate вместо bundle exec rake db:migrate, но не думаю, что это могло бы существенно повлиять.

Я никогда не использовал docker stack или swarm. Есть ли шанс, что где-то в твоих скриптах есть ошибка, из-за которой скрипт migrate.sh использует старый образ вместо нового?

Я явно это не проверял, займусь этим. Кластер точно использует последний образ, но, возможно, скрипт CI по какой-то причине не использовал его.

Я проверил это сейчас с обновлением 3.1.1. Действительно, скрипт CI использовал более старую версию контейнера.