В этой статье я хочу поделиться несколькими удобными способами организации вашего проекта на рабочем (даже продакшен) сервере.
Я работаю, в основном, с Python/Django стеком, поэтому все примеры будут, в первую очередь, применительно к этому набору. Также ключевые технологии: Ubuntu (17.10), Python3 (3.6).
Содержание:
- Логи (logrotate)
- Демоны (systemd)
- локальные настройки
Предполагается что вы делаете все грамотно, приложение хранится в репозитории, деплоится в отдельную папку на сервере, используется, например, virtualenv. Для запуска используется отдельно созданный юзер, который имеет достаточно прав, но не слишком много (например не имеет sudo и не разрешен логин по ssh).
Для начала я повторю прописные истины, что все что хранится в репозитории — это только чистый код и статичные данные. Все настройки содержат только дефолты, никаких паролей и ключей, даже в неиспользуемых файлах.
Даже на рабочем компьютере (ноутбуке) где вы пишете код у вас в папке проекта не должно быть ничего что бы вы не могли закачать на продакшен. Имеется в виду порочная практика использования файлика "local_settings.py" в папке settings внутри проекта (как вариант — development_settings.py). Я разберу этот пример ниже.
Логи
Наверняка вы используете логирование. Встроенный модуль logging очень хорош, но не всегда стоит изощряться и использовать его для всего на свете.
Например, ротация логов. В интернете попадаются сложные и изощренные способы начиная от стандартного RotatingFileHandler и заканчивая написанием собственного демона на сокетах для записи логов из нескольких источников. Проблемы начинаются из-за желания делать все на "чистом Python". Это глупо и неэффективно, зато приносит кучу возможных мест возникновения ошибок.
Используйте сервис logrotate. Ниже приводится простой конфиг для логов celery.
Стандартными средствами пишем файлик /var/log/myproject/celery.log, ежедневно он кладется в папку /var/log/myproject/archive/ и к имени добавляется суффикс предыдущего дня.
/var/log/myproject/celery.log {
size 1
su myuser myuser
copytruncate
create
rotate 10
missingok
postrotate
timeext=`date -d '1 day ago' "+%Y-%m-%d"` # daily
# timeext=$(date +%Y-%m-%d_%H) # hourly
mv /var/log/myproject/celery.log.1 /var/log/myproject/archive/celery_$timeext.log
endscript
}
Если у вас лог пишется очень быстро и вы хотите его ротировать каждый час, то в конфиге перекомментируйте строчки "daily" и "hourly". Также нужно настроить logrotate чтобы он запускался каждый час (по умолчанию обычно ежедневно). Выполните в bash:
sudo cp /etc/cron.daily/logrotate /etc/cron.hourly/logrotate
sudo sed -i -r "s/^[[:digit:]]*( .+cron.hourly)/0\1/" /etc/crontab
Конфиг (файлик myservice) надо положить в папку logrotate
sudo cp config/logrotate/myservice /etc/logrotate.d/myservice
важные моменты:
- конфиг надо именно скопировать, симлинки работать не будут
- в принципе, конфиг почти взят из доки по logrotate, но очень важно поставить
copytruncate
директиву - (добавлено из комментария rusnasonov ) logrotate туповат и использует простейшую систему ротирования, без буферов. Ротирование проходит в два шага — сначала копируется старый файл, а потом он обрезается на старом месте. Это может привести к потере логов, которые были записаны в промежутке (отражено в документации)
copytruncate важна по той причине, что при ротировании файл не будет закрыт. Поскольку мы ротируем файл на "живой" системе, файл открыт и сервисы, которые в него пишут, делают это по какому-то файловому дескриптору. Если вы просто переместите файл и создадите новый пустой, то он не будет использоваться. copytruncate говорит что нужно скопировать содержимое, а потом очистить файл, но не закрывать.
Сервисы
Как вы запускаете ваше приложение? Есть куча разных способов. По моим наблюдения основные такие:
- запускаем screen/tmux и внутри запускаем в интерактивном режиме скрипт
- режим демона типа "-D" для gunicorn или celery
- supervisord
- init.d скрипт
- Docker
Все они имеют свои плюсы, но, на мой взгляд, они имеют еще больше минусов.
Я не буду здесь рассматривать Docker. Во-первых, у меня не так много опыта работы с ним. А во-вторых, если вы используете контейнеры, то вам и остальные советы из этой статьи не очень нужны. Там подход уже другой.
Я считаю, что если система предоставляет нам удобный инструмент, то почему бы им не воспользоваться.
В Ubuntu начиная с версии 15.04 по-умолчанию поставляется systemd для управления сервисами (и не только).
systemd очень удобен тем, что самостоятельно делает все правильно:
- запускает приложение под нужным пользователем и устанавливает переменные окружения, если нужно
- при внезапной остановке приложения — перезапускает его заданное количество раз
- очень гибок и позволяет настроить зависимости и порядок запуска
Конечно, если у вас нет systemd, то можно смотреть в сторону supervisord, но у меня довольно большая нелюбовь к этому инструменту и я избегаю его использовать.
Я надеюсь не будет людей, кто будет сомневаться что при наличии systemd использовать supervisord вредно.
Ниже я приведу пример конфига для запуска.
Запуск gunicorn (проксируется через локальный nginx, но это здесь неважно).
[Unit]
Description=My Web service
Documentation=
StartLimitIntervalSec=11
[Service]
Type=simple
Environment=DJANGO_SETTINGS_MODULE=myservice.settings.production
ExecStart=/opt/venv/bin/python3 -W ignore /opt/venv/bin/gunicorn -c /opt/myservice/config/gunicorn/gunicorn.conf.py --chdir /opt/myservice myservice.wsgi:application
Restart=always
RestartSec=2
StartLimitBurst=5
User=myuser
Group=myuser
ExecStop=/bin/kill -s TERM $MAINPID
WorkingDirectory=/opt/myservice
ReadWriteDirectories=/opt/myservice
[Install]
WantedBy=multi-user.target
Alias=my-web.service
Здесь важно не использовать режим демонизации. Мы запускаем gunicorn обычным процессом, а демонизирует его сам systemd, он же и следит за перезапуском при падениях.
Обратите внимание, что мы используем путь к python и gunicorn относительно virtualenv-папки.
Для celery все будет таким же, но строку запуска я рекомендую такой (пути и значения поставьте свои):
ExecStart=/opt/venv/bin/celery worker -A myservice.settings.celery_settings -Ofair --concurrency=3 --queues=celery --logfile=/var/log/myservice/celery.log --max-tasks-per-child 1 --pidfile=/tmp/celery_myservice.pid -n main.%h -l INFO -B
Стоит обратить внимание на параметры для перезапуска:
StartLimitIntervalSec=11
RestartSec=2
StartLimitBurst=5
Вкратце это означает следующее: если сервис упал, то запусти его снова через 2 секунды, но не больше 5 раз за 11 секунд. Важно понимать, что если значение в StartLimitIntervalSec будет, например, 9 секунд, то в случае если сервис остановится 5 раз подряд (сразу после запуска), то после пятого падения systemd сдастся и не будет его больше поднимать (2 * 5). Значение 11 выбрано именно с тем, чтобы исключить такой вариает. Если, например, у вас был сетевой сбой на 15 секунд и приложение падает сразу после старта (без таймаута), то пусть уж лучше оно долбит до победного, чем просто останавливается.
Чтобы установить этот конфиг в систему, можно просто сделать симлинк из рабочей папки:
~~sudo ln -s /opt/myservice/config/systemd/*.service /etc/systemd/system/~~
sudo systemctl daemon-reload
Однако, с симлинками надо быть осторожными — если у вас проект лежит не на системном диске, то есть вероятность что он может монтироваться после старта сервисов (например, сетевой диск или memory-mapped). В этом случае он просто не запустится. Здесь вам придется гуглить как правильно настроить зависимости, да и вообще конфиг тогда лучше скопировать в папку systemd.
Update: после замечания andreymal я думаю что будет правильнее копировать конфиги в папку и ставить им правильные права:
sudo chown root: /opt/myservice/config/systemd/*.service
sudo chmod 770 /opt/myservice/config/systemd/*.service
sudo cp /opt/myservice/config/systemd/*.service /etc/systemd/system/
sudo systemctl daemon-reload
Еще советую отключить вывод в консоль, иначе все будет попадать в syslog.
Когда у вас все компоненты заведены в systemd, то использование каждого из них сводится к:
sudo systemctl stop my-web.service
sudo systemctl stop my-celery.service
sudo systemctl start my-web.service
sudo systemctl start my-celery.service
Когда компонентов больше одного, то имеет смысл написать скрипт для массового управления всем хозяйством на сервере.
bash manage.sh migrate
bash manage.sh start
Для удаленной отладки через консоль (запустит shell_plus из django-extensions):
bash manage.sh debug
Если у вас больше одного сервера, то, конечно, вы используете уже свои скрипты для разворачивания, это просто пример.
Локальные настройки
Это тема для холивара и я хочу заметить, что этот параграф — исключительно моя точка зрения, которая базируется на моем опыте. Делайте как хотите, я просто советую.
Суть проблемы в том, что как ни пиши приложение, ему, скорее всего, придется иметь дело с данными, которые где-то хранятся. Список юзеров, путь к ключам, пароли, путь к базе, и тп… Все эти настройки хранить в репозитории нельзя. Кто-то хранит, но это порочная практика. Небезопасно и негибко.
Какие есть самые частые способы хранения таких настроек и какие проблемы с ними:
- файлик local_settings.py, который хранится в папке проекта рядом с дефолтными setting.py
Проблема: можно случайно закоммитить файл, можно его затереть при копировании/обновлении папки (rsync или из архива) - переменные окружения. Тоже не очень безопасно, не очень удобно (при soft-reload например)
- отдельный файл вне папки проекта
Я рекомендую именно этот способ. Обычно я для проекта создаю yaml-файл в папке "/usr/local/etc/". У меня написан небольшой модуль, который используя магию хаки загружает переменные из файлика в locals()
или globals()
импортирующего модуля.
Используется очень просто. Где-то в глубинах settings.py для Django (лучше ближе к концу) достаточно вызвать:
import_settings("/usr/local/etc/myservice.yaml")
И все содержимое будет замешано в глобальные settings. У меня используется merge для списков и словарей, это может быть не всем удобно. Важно помнить, что Django импортирует только UPPERCASE константы, то есть в файлике настройки первого уровня у вас сразу должны быть в верхнем регистре.
That's all folks!
Остальное обсудим в комментариях.
Комментарии (78)
ebt
20.03.2018 03:45Пожалуйста, поясните, почему вам не подходит supervisord.
markoni
20.03.2018 12:45-1Видимо, потому, что supervisord не подходит к python3. Во всяком случае, какое-то время не подходил (какие-то костыли там пилили, да), поэтому мной, например, был отправлен на свалку.
banzayats
20.03.2018 12:51IMHO, зачем нужна еще одна сущность в виде supervisord, если все можно решить исползуя возможности systemd?
Ну и автор написал, что "если у вас нет systemd, то можно смотреть в сторону supervisord".
skorpix
20.03.2018 16:20+1А зачем использовать сторонние решения, если все есть из коробки? Тем более systemd вроде стабильный. Если у вас есть какие-то плюсы supervisord по сравнению с systemd или специфичные случаи использования, поделитесь, мне интересно. Мне для Django+Celery хватает systemd за глаза.
baldr Автор
20.03.2018 16:22Собственно, уже ответили. supervisord — это еще одна программа, приносящая свои собственные баги. Ждать что она будет надежнее чем системный сервис смысла нет, да и нет у него того что не может systemd.
EgorLyutov
20.03.2018 09:35Если уж деплоить приложение без докера, то лучше тогда уж использовать какой-нибудь ansible проект, который разворачивает проекты. Секреты хранить с помощью ansible vault, например. Если используется gitlab и местный CI, то так же загружать можно в настройки CI секреты. Я последнее время использую три окружения для проектов: dev, staging, prod. И уже в зависимости от этой переменной окружения строится окончательный settings.py. То есть мне не нужно заходить на сервер и править там конфиги вручную. Если проект небольшой, то просто docker-compose и на сервере .env файл со всеми переменными.
SirEdvin
20.03.2018 12:24Используйте сервис logrotate. Ниже приводится простой конфиг для логов celery.
Почему не journald, если у вас уже есть systemd? Просто пишете в stdout и профит. И ротация из коробки.
переменные окружения. Тоже не очень безопасно, не очень удобно (при soft-reload например)
Эм… что? У вас же systemd, там есть EnvironmentFile, очень удобно. И особой разницы между local.py и этим решением с точки зрения безопасности нет. Если хотите безопасности, вперед, в vault.
Зато настройки, а вернее, хранение только sensetive данных в настройках окружения, а всего остального в файле в проекте позволяет легко узнать разработчикам, какие настройки используются. И это довольно часто бывает важно.
baldr Автор
20.03.2018 16:39Насчет journald — да, это хорошая идея, спасибо. Надо бы почитать доки и попробовать.
Однако не очень кроссплатформенно. Если приложение пишет в файл, то это будет работать везде, а уж что делать с логами — можно решить системными инструментами (logrotate).
Про локальные параметры я написал что это только моя рекомендация. Каждый делает как удобно в его проекте.
Я встречал совершенно разные подходы — и settings_ivanov.py / settings_stage.py в репозитории, и замену settings.py на нужный при переустановке.
Меня учили отделять данные от кода и я считаю что это правильно.
Переменные окружения легко не перечитаешь без перезапуска, в отличие от конфига. Да и при запуске любого дочернего процесса он наследует все переменные, что не всегда хорошо. Мне это не нравится и я не использую.SirEdvin
20.03.2018 16:44Однако не очень кроссплатформенно.
Кроссплатформенно, так как вы пишите в stdout. А обрабатывать stdout можно уже инструментами платформы, в нашем случае это journald. На винде есть какие-то другие штуки. Ну и более того, вполне можно предположить, что ваше приложение все равно не кросс-платформенно.
Меня учили отделять данные от кода и я считаю что это правильно.
Простите, какие данные? Вы предлагаете по не совсем ясным причинам отделять настройки приложения от самого приложения. Какой профит вы от этого получаете? Минусы я знаю, они заключаются в том, что надо каждый раз дергать человека, который занимается деплоем, что бы он правил файл настроек.
baldr Автор
20.03.2018 17:05можно предположить, что ваше приложение все равно не кросс-платформенно.
Предположение не очень обоснованно. У меня есть кросс-платформенные проекты. На windows запускается django-приложение под апачем через WSGI и вот чего я точно не хочу, так это логов приложения в логах апача.
На мой взгляд, модуль logging отлично справляется с выводом в файлы (разные) и эти файлы удобно обрабатывать (чем-то сторонним), но смешивать я не советую.
Простите, какие данные?
Настройки — это данные.
У меня идея следующая: в папке проекта не должно быть никаких файлов, которые изменяются в период между обновлениями версии. Код — отдельно, данные, логи, временные файлы, конфиги — в своих папках.
Далеко не всегда получается так что весь проект написан на одном языке и чтение параметров из единого json/yaml/cfg файла удобнее.SirEdvin
20.03.2018 18:56в папке проекта не должно быть никаких файлов, которые изменяются в период между обновлениями версии.
Эм… а код? Стоит заметить, что очень часто код и настройки меняются одновременно, потому что добавляется новый функционал, который как бы нужно настраивать.
Далеко не всегда получается так что весь проект написан на одном языке и чтение параметров из единого json/yaml/cfg файла удобнее.
Эм… это как у вас так получается проект на разных языках? Вызов скриптов на других языках?
baldr Автор
20.03.2018 21:14Тем не менее, код — сам по себе, настройки — отдельно. Если вы посмотрите в файловую систему Linux, вы примерно таким образом организованные файлы и найдете, это не я придумал.
Эм… это как у вас так получается проект на разных языках? Вызов скриптов на других языках?
Ну а что такого? Чем меньше языков, тем проще, конечно, но не всегда так получается. Особенно когда в проект приходят новые творческие участники.SirEdvin
20.03.2018 23:16Тем не менее, код — сам по себе, настройки — отдельно. Если вы посмотрите в файловую систему Linux, вы примерно таким образом организованные файлы и найдете, это не я придумал.
Да, и поэтому каждый пакет тянет с собой настройки или же приложение заполняет дефолтные настройки. А потом еще надо при баг-репортах добавлять конфиги, иначе ничего не понятно.
Но у вас то окружение, которое вы контролируете, можно позволить себе больше в угоду удобству.
bykvaadm
20.03.2018 17:00-1ну, вот поэтому вы и воротите всякую фигню, потому что в докер не умеете =)
baldr Автор
20.03.2018 17:07Ага, докер, блокчейн, devops, go и еще несколько модных слов в каждом проекте помогают сохранить лицо перед начальством и добавить уверенности в отчеты инвесторам.
SirEdvin
20.03.2018 19:04Да, гораздо лучше настраивать все на сервере исключительно ручками и там хранить настройки. Когда вы один, можно вообще делать любую дичь и все будет отлично, а вот как только начинается нормальная работа, то вот тут приходится делать по нормальному. И так уж получилось, что контейнеризация (в том числе и докер) заставляет задуматься о том, что такое нормально немного раньше.
Быстро находится смысл и желание хранить настройки в проекте приложения, что бы они были всем доступны (кроме паролей и доступов), и проблемы с логами решаются, и с запусками скриптов и еще ряд прочих.
viiy
20.03.2018 19:53:sarcasm:
Вы забыли kubernates (какой же докер без него?)
И aws. Тогда уж и terraform.
gitlab, ansible
Плюс consul, vault
Что еще модно… ceph как block storage
Мониторинг, прометеус, elk для логов
Своя автономная зона, cdn, сеть фильтрации трафика и ddos
Кавку куданибудь там сбоку надо обязательно, как вишенку на торт
Closius
20.03.2018 18:59+1Я бы еще добавил, что когда разрабатываешь проект параллельно пиши например bash скрипт, где ты описываешь как все ставишь. Таким образом при переустановки системы или переезде на другой сервер можно за пару минут все поставить без боли. Возможно для больших и серьезных проектов надо что-то по серьезнее типа докера или ansible как уже выше говорили.
SirEdvin
20.03.2018 19:13Для любых проектов нужен ansible или другая система. Я понимаю предупреждения против докера, особенно на мелких проектах, но если вы не используете ansible или альтернативы — вы делаете неправильно.
Closius
20.03.2018 19:17А как же люди жили до Ансибля (он появился в 2012)?
SirEdvin
20.03.2018 19:21+1Был, например, CFEngine. А потом еще Chef и puppet. До него жили на самописных скриптах и жили плохо, да.
А еще люди умирали 1000 лет назад в 30 лет в том числе от антисанитарии. Будете ли иронизировать о том, как же люди жили раньше без мыла?
У вас будут реальные аргументы, почему не стоит использовать ansible на мелких проектах, кроме "Ой, сложна"? Поверьте, оверхед по времени ближе к середине проекта выравнивается, а если вы уже имеете опыт его использования, то самописные скрипты требуют значительно больше времени на кастомизацию.
Closius
20.03.2018 19:33+1Ну так знаете можно дохрена чего нагородить в маленький проект. И докер и систему мониторинга и риал тайм мониторинг с отправкой сообщений на приватный телеграм канал… Все зависит от потребностей. Я ни в коем случае не говорю, что ансибль плохо. Просто не вижу смысл ставить его приоритетным для маленького проекта, как минимум ансибль это дополнительная сущность, а чем меньше сущностей используешь, тем надежнее. Когда уже будет понятно, что проект будет разрастаться, можно и ансибль прикрутить. Научиться использовать его конечно будет не лишним.
У меня бывало проекты умещались меньше чем на 50 строк баш скрипта, нафиг мне тут ансибль?SirEdvin
20.03.2018 19:39Вы пишете доку по тому, как развернуть ваш проект? Вот ansible это дока, только которая еще и выполняется.
В добавок к этому размер ansible скриптов всегда четко следует размерам проекта или связанной инфраструктуры. Если у вас просто баш скрипт, то вы или выгружаете его на сервер через ansible или разворачиваете там git репозиторий с проектом (и ключи для него). И все, это делается довольно быстро.
Ну и "а чем меньше сущностей используешь, тем надежнее", подразумевает, что вы не будете делать то, что делает ansible, но нет, у вас все равно будут самописные скрипты по деплою. И вместо того, что бы использовать продукт, который для этого разрабатывался вы предлагаете на коленке напилить ad-hoc решение, у которого не будет никаких преимуществ. Зачем?
andreymal
20.03.2018 19:41+1sudo ln -s /opt/myservice/config/systemd/*.service /etc/systemd/system/
Взламываем сайт, залезаем в файл
/opt/myservice/config/systemd/*.service
(который на запись естественно никто не закрыл), меняем в файлеUser=myuser
наUser=root
иExecStart
по вкусу — опа, рут успешно получен хацкером.baldr Автор
20.03.2018 21:18Хороший пример, спасибо. Да, наверное тогда правильнее будет скопировать вместо симлинков и поставить права?
andreymal
20.03.2018 21:22Вроде как сервисы в systemd вообще может создавать сам пользователь без вмешательства рута (где-то в
~/.config/systemd
если верить арчевики), но я тут не спец, сам я по старой привычке юзаю supervisord с копированием конфигов)
rusnasonov
20.03.2018 21:18+1Еще бы добавил в важные моменты логирования — copytruncate в logrotate сначала делает копию, а потом урезает файл. Все строки, которые будут записаны в лог после копирования но перед урезанием успешно потеряются.
marazmiki
21.03.2018 06:42Вот Вы утверждаете, что supervisord — лишняя сущность и надо её избегать. Но при этом рекомендуете файлы с логами ротейтить, а конфиги хранить не в переменных окружения, а в файлах. Да ещё и велосипед какой-то для этого придумали. На мой взгляд, это двойные стандарты :)
Если уж избавляться от лишних сущностей, то по-большому. Тезисно:
- Логи писать исключительно в
stdout
; - Если есть
systemd
, использовать его. Если нет, обновлять сервер, чтобы был. Или менять на другой. Ни в коему случае не использоватьsupervisord
сотоварищи. - Контейнеры по возможности использовать, причём с readonly-файловой системой.
- Хранить настройки в переменных окружения
Здесь, напоминаю, только тезисы. Давайте более подробно обсудим в комментариях?
P.S. Хабраюзер ahmpro уже упоминал про 12 факторов. Рекомендую обратить внимание и почитать. Соглашаться со всем не обязательно, но крайне полезно для знакомства.
marazmiki
21.03.2018 06:55Почему не надо писать логи в файл
- Потому что писать на жёсткий диск долго;
- Потому что запись в файловую систему привязывает нас к собственно файловой системе.
- Потому что приложение обязано сообщать о произошедших событиях, а не сохранять их.
Почему надо писать логи в STDOUT
- Потому что это стандартный вывод. Он есть везде и везде более-менее одинаково работает
- Потому что стандартным выводом процесса может управлять файловая система (или пользователь). Этот стандартный вывод можно грепать в реальном времени; его можно писать в journald, его можно отправлять в fluentd, его даже можно писать в файл и логротейтить :)
Но заниматься этим должно не наше приложение.
marazmiki
21.03.2018 07:01Почему не нужно использовать
supervisord
- Потому что внешняя зависимость, без которой можно обойтись
- Потому что глючный
- Потому что python2
Ещё, помнится, был проект circus. Что-то типа
supervisord
, но для Python3. В бою не испытывал, да и смысла особого нет, поскольку, согласно бритве Оккама, лучше пользоваться тем, что есть.andreymal
21.03.2018 10:37Потому что глючный
Как активно пользующий supervisord заявляю, что нет
Потому что python2
Как кодящий на обоих питонах заявляю, что ничего плохого в этом нет
(с первым пунктом не спорю)
marazmiki
21.03.2018 11:43Я буквально на днях переносил старую кодовую базу на новое железо и старался использовать тот же софт, что и раньше. Так вот, как
supervisord
не умел 4 года назад убивать воркерыcelery
при перезапуске задачи, так и не умеет до сих пор. В принципе, этого одного уже достаточно, чтобы им не пользоваться :-)andreymal
21.03.2018 11:45Зачем supervisord'у убивать воркеры celery? Ему нужно убить лишь мастер-процесс, а мастер-процесс должен убить всех своих воркеров самостоятельно — именно так у меня работает celery внутри supervisord уже несколько лет, всё прекрасно и не глючит.
marazmiki
21.03.2018 11:54Виноват, а воркер целери не может быть мастер-процессом?
andreymal
21.03.2018 12:01Я не разбирался в деталях, как работает celery и что он может, но я его сейчас запускаю как
celery -A myapp worker --concurrency=2 -B
и это запускает четыре процесса: мастер, воркер, воркер и celerybeat. Когда я отправляю SIGINT мастер-процессу, остальные три процесса автоматически завершаются следом за ним.
marazmiki
21.03.2018 07:04Почему лучше использовать контейнеры.
- Потому что если выломают приложение, пострадает только контейнер, а на хост-система
- Потому что можно добиться идентичного окружения в на бою и на машине разработчика, даже если на сервере Linux, а у разработчика — macos (или windows, ни к ночи упомянута будет).
- Разработка в контернере с файловой системой, примонтированной в ReadOnly дисциплинирует: приходится писать так, чтобы не привязываться к собственно файловой системе. Поэтому приложения получаются такими, что их легче масштабировать
- Это, чёрт возьми, модно!
andreymal
21.03.2018 10:43Потому что если выломают приложение, пострадает только контейнер, а на хост-система
Если выломают приложение, то пострадает только приложение, если руки не кривы нужные юниксовые права правильно настроить
Потому что можно добиться идентичного окружения
Идентичное окружение — зло. Хорошо сделанное приложение должно одинаково хорошо работать не а каком-то ОДНОМ, а а ЛЮБОМ окружении — от фряхи на большом сервере до малинки в ближайшей кофеварке. Если разработчик не в состоянии обеспечить работу приложения за пределами одного окружения, это указывает на кривизну его рук, а контейнеры лишь прикроют говнокод в данном случае. (Есть, конечно, редкие исключения, когда делать работоспособность в любом окружении слишком геморройно, но например обычные веб-сайты абсолютно точно не в их числе.)
Контейнер всё стерпит, агаahmpro
21.03.2018 11:30Под идентичное окружением имелся ввиду один и тот же набор (с совпадением вплоть до минорных версий) библиотек и системных зависимостей, необходимых для работы приложения, речи про где запускается код в данном случае не идет.
Сейчас все стараются изолировать код от системного окружения где это запускается (12 факторов этому способствует), то на что вы собственно и обратили внимание.
Контейнеры лишь удобный механизм сделать это с минимальными трудозатратами, сводя к минимуму необходимость разработчику думать о том, где его код будет запущен.
То, что некоторые юзают контейнеры по старинке, запихивая всякую фигню в них, то дело в некомпетентности, а не в самих контейнерах.
Это, чёрт возьми, модно!
Мода лишь следствие удобства, которое контейнеры дают. Да, не бесплатно, но кто говорил, что это серебряная пуля? :Dandreymal
21.03.2018 11:35Под идентичное окружением имелся ввиду один и тот же набор (с совпадением вплоть до минорных версий) библиотек и системных зависимостей
Если нужно воспроизвести баг, который в других условиях не воспроизводится, то это хорошо. Но опять же затачиваться целиком на одну конкретную версию плохо: хорошо сделанное приложение должно работать в пределе на ЛЮБЫХ версиях зависимых библиотек, тем более в пределах одной мажорной версии (суровая реальность, конечно, накладывает ограничения из-за всяких там багов и обратной совместимости, но стремиться к поддержке по возможности больше числа версий нужно). Идеальное по моему мнению приложение должно работать где угодно от устаревшей центоси до самой свежей убунты, если нет уважительных причин для иного (лень разработчиков — не уважительная причина).
ahmpro
21.03.2018 12:10Идеальное приложение не имеет зависимостей и вообще написанного кода, так что давайте без фантазий :D
marazmiki
21.03.2018 12:04+1Если выломают приложение, то пострадает только приложение, если руки не кривы нужные юниксовые права правильно настроить
А вдруг кривые. Зачем оставлять потенциальную возможность?
Идентичное окружение — зло.
Голословное утверждение. И в корне неверное. Хорошее приложение должно делать только одно: выполнять поставленную бизнес-задачу с разумными затратами русурсов (времени, денег, мощностей). Всё остальное — пожелания, причём не всегда критичные.
Даже если в контейнере говнокод — да ради бога, если он работает, не просит каши и его можно поддерживать. Действительно, контейнер всё стерпит. Хорошее правило.
Требование же чтобы работало везде… А зачем?
andreymal
21.03.2018 12:07-2А вдруг кривые
Тогда и контейнер не поможет :D
Действительно, контейнер всё стерпит. Хорошее правило. Требование же чтобы работало везде… А зачем?
Вот примерно из-за такого мнения многих людей я хочу свалить с этой прогнившей планеты подальше на какой-нибудь Марс. Все живут по принципу фигак-фигак и в продакшен, совсем чувство прекрасного растеряли, нафиг так жить
marazmiki
21.03.2018 12:22Если руки кривые (а это бывает… ох бывает), то контейнер кокрастыке поможет.
Вот примерно из-за такого мнения многих людей я хочу свалить с этой прогнившей планеты подальше на какой-нибудь Марс. Все живут по принципу фигак-фигак и в продакшен, совсем чувство прекрасного растеряли, нафиг так жить
Из-за какого мнения? Программирование — это не искусство, это прикладная область, за неё кушать дают. А хочется прекрасного — участвуйте в олимпиадах. Денег не заплатят, ну так художник и должен быть голодным :)
andreymal
21.03.2018 12:26Программирование — это не искусство
Вот примерно из-за такого мнения и хочу свалить :)
А хочется прекрасного — участвуйте в олимпиадах
А я участвовал, в них обычно как раз лютейший фигак-фигак-и-в-продакшен)
andreymal
21.03.2018 11:17даже если на сервере Linux, а у разработчика — macos
А отличная от линукса ОС, кстати, автоматически означает необходимость использования виртуальной машины (Docker на макоси использует VirtualBox, а на винде Hyper-V, если я правильно понял его документацию). Но виртуалка по сути ведь является контейнером тоже — зачем пихать контейнер внутрь контейнера? Можно выкатить ansible-плейбук на убунточку, запихнутую в виртуалбокс, и получить идентичное продакшену окружение — я для своих сайтов делаю именно так.
ahmpro
21.03.2018 11:34vagrant, provisioning энсимблом и автоматический накат дампов с лайва(или транка) просто удобнейший способ локальной разработки, для нового разработчика это пара команд:
git clone
vagrant up
./setup.sh (внутри виртуалки, если проект достаточно сложный и требуется донастройка, которая тоже автоматическая и заранее написана, используя теже ansible-роли)
marazmiki
21.03.2018 07:21Про хранение настроек
На мой взгляд, среди всех перечисленных способов Вы порекомендовали самый неудачный. По сути, он имеет все те же самые минусы, что и
local_settings.py
, кроме одного: его случайно не поместить в репозиторий. Да и то плюс этот сомнительный, с учётом.gitignore
Зато привносит дополнительные минусы:
- Магия. Сами упомянули.
- Лишняя зависимость. Не столько даже
yaml
, сколько необходимость размещать этот "магический" код в каком-то файле, чтобы его импортировать - Захардкоженные пути. По идее, они должны быть одинаковы, но где, к примеру, искать
/usr/local/etc/
в Windows? :) Или логику городить? Тогда почему бы просто не остановиться наlocal_settings
?
Переменные окружения таких проблем не продуцируют.
- Они специально придуманы для конфигурации окружения, в котором запускается процесс. Конфигурация от народа!
- Их поддержка есть в каждой операционной системе
- Их использование всегда одинаково и на windows, и в macos, и в linux, и в докер-контейнерах.
- Они случайно не попадут в версионный контроль
- Они прекрасно интегрируются с тем же
systemd
Про какие неудобства идёт речь — я так и не понял. И почему они менее защищены чем файл, лежащий снаружи репозитрия.
И вот этот Ваш коммментарий:
Переменные окружения легко не перечитаешь без перезапуска, в отличие от конфига. Да и при запуске любого дочернего процесса он наследует все переменные, что не всегда хорошо. Мне это не нравится и я не использую.
тоже не понял. Особенно в контексте джанги выглядит странно, где конфиг вычисляется при старте приложения.
baldr Автор
21.03.2018 15:48Да, вы все правильно написали, но, простите, однобоко.
Я уже приводил аргументы, не вижу смысла снова повторять, тем более гуглится довольно много еще аргументов почему использовать переменные окружения не все рекомендуют.
тоже не понял. Особенно в контексте джанги выглядит странно, где конфиг вычисляется при старте приложения.
Ок, давайте возьмем тот же gunicorn с его child-процессами. Если вы подложили новый код (или новые настройки даже) и выполнили «kill -HUP», то он аккуратно перезапустит воркеры и они начнут выполняться уже с новым кодом, трафик потерян не будет. Однако переменные окружения будут те же самые, что и при старте мастера. И поменять их у мастера вы не сможете без рестарта процесса.marazmiki
21.03.2018 16:16Окей, про gunicorn принимается, убедили, хотя случай довольно странный.
Других аргументов, извините, не увидел. И по старинному правилу «бремя доказательства лежит на обвиняющем», прошу всё же показать аргументы :)baldr Автор
21.03.2018 17:10+1Ну еще пример с переменными окружения там где дочерние процессы наследуют окружение родителя. Запустили вы в subprocess какой-нибудь внешний процесс, а он получил все ваши пароли, а потом в крэшдампе отправил своим разработчикам.
В сети бродят страшилки про «ps -eww », но у меня не получилось повторить.
Можно привести из пальца высосанный вектор атаки когда, получив доступ к выполнению кода с непривилегированным пользователем можно попробовать почитать все файлы в /proc/<все pidы>/environ (для процессов из-под этого пользователя), но слишком маловероятна удача.
Все перечисленные в этом комменте проблемы лечатся запуском из-под другого пользователя и очисткой переменных окружения перед запуском, но все ли так делают?
Я реально не вижу удобства в переменных окружения. Мне лично удобнее прочитать переменную DATABASES как dict из yaml/json файла чем составлять ее из нескольких переменных окружения.marazmiki
21.03.2018 18:10Запустили вы в subprocess какой-нибудь внешний процесс, а он получил все ваши пароли, а потом в крэшдампе отправил своим разработчикам.
Вроде бы в
subprocess
можно создавать чистое окружение, но аргумент всё же
принимается, спасибо.
Я реально не вижу удобства в переменных окружения. Мне лично удобнее прочитать переменную DATABASES как dict из yaml/json файла чем составлять ее из нескольких переменных окружения.
Хе-хе. Если б одна запись составлялась из нескольких переменных, это не было так круто, как есть. Жизнь — она горазда элегантнее.
Уже стало хорошей традицией записывать все кренделя в виде DSN
DATABASE_URL=postgres://user:supersecretpassword@hostname.com:5432/db?timeout=20 ANOTHER_DATABASE_URL=mysql://user:passwrd@mysqlserver.com/mysqldb
а настройки выглядят так:
import dj_database_url as db_config # да, это внешняя зависимость. dj-database-url DATABASES = { 'default': db_config.config(), 'another': db_config.config(var='ANOTHER_DATABASE_URL'), }
Однажды видел, правда, не помню где, как особо одарённые люди делали так:
DATABASES = parse_env()
В результате этой надстройки в
DATABASES
под ключомdefault
попадало распарсенное значениеDATABASE_URL
, а все переменные видаMYPREFIX_DATABASE_URL
попадали туда же как значение ключаmyprefix
.
Совершенно аналогично задаются кренделя к e-mail, кешам, очередям типа rabbitmq и прочая, и прочая, и прочая.
С примитивами тоже наглядно. Понятно, что переменная окружения всегда строка (если есть) и
None
, если не определена. Но многочисленные обвязки позволяют писать очень, на мой взгляд, довольно прикольно. Например, так:
SECRET_KEY = env('SECRET_KEY', cast=str, default='') DEBUG = env('DEBUG', default=False, cast=bool) ALLOWED_HOSTS = env('ALLOWED_HOSTS', cast=list, default=['*']) # разделяет строку по запятой
или так:
SECRET_KEY = env.string('SECRET_KEY') DEBUG = env.boolean('DEBUG')
кто во что горазд, короче.
Разумеется, это тоже внешняя зависимость, как и в Вашем случае. Но всё же, по чесноку, что более выразительно и явно:
import_settings("/usr/local/etc/myservice.yaml")
или
DEBUG = env('DEBUG', default=False) SECRET_KEY = env('SECRET_KEY_FROM_MY_ENV_VAR')
? Мы же помним, что явное лучше неявного. Кто его знает, какие переменные понаинжектили злобные русские хакеры в конфиг, который Вы инклюдите? :)
andreymal
21.03.2018 18:24У меня тут на одном моём сайте два десятка настроек только из тех, что переопределены для боевого окружения, плюс при желании можно насчитать ещё полсотни-сотню настроек со значениями по умолчанию. Лично мне запихивать это всё в переменные окружения было бы не очень удобно)
marazmiki
21.03.2018 18:34Ну да, засунуть эти оверрайды в отдельный файл — гораздо более продуманная тактика :)
baldr Автор
21.03.2018 18:36А как бы вы сделали с переменными окружения? Куда бы вы их засунули?
marazmiki
21.03.2018 19:21Куда бы я засунул переменые окружения? Сложный вопрос. Наверное, я бы их и оставил в переменных окружения :)
andreymal
21.03.2018 19:23Но переменные окружения вроде ведь не берутся сами по себе из воздуха? Откуда они появляются?
marazmiki
21.03.2018 19:26Их выставляет пользователь. Или сама система. Или их передаёт процесс-родитель. Возможны варианты, короче. Я суть вопроса, признаться, не уловил
andreymal
21.03.2018 19:29Выставляет пользователь где и как? Процесс-родитель у себя их откуда берёт? Какие именно варианты возможны?
Я суть вопроса, признаться, не уловил
Вот у меня двадцать условных параметров для сайта и некий ваш условный контейнер, например — как конкретно мне запустить «неважно что там: gunicorn, ./manage.py shell_plus, ./manage.py migrate» с этими вот двадцатью параметрами с использованием этих ваших переменных окружения?
baldr Автор
21.03.2018 19:38Да они точно так же пишут их в файл манифеста для докера, только добавляют там еще больше настроек для самого докера.
marazmiki
21.03.2018 19:40Вот прям нутром чую, что подвох. Но отвечу как есть
Самое простое — перечислить их в командной строке:
$ DEBUG=1 SECRET_KEY=123 ./manage.py migrate $ DEBUG=1 SECRET_KEY=123 ./manage.py shell_plus
Или выставить их в текущий сеанс. Тогда они будут работать для всех процессов, порождённых текущей сессией шелла
export DEBUG=1 export SECRET_KEY=123 $ ./manage.py migrate $ ./manage.py shell_plus
Если речь о докере, то там при запуске можно передать ключ типа
--env-fle=/path/to/env
. Обычный текстовый файл с парамиKEY=VALUE
по одной на строку.
Для systemd можно такой же файл указать. Для питона существует куча пускалок, которая смотрит, нет ли файла .env в текущей директории. И если есть, считывает переменные и помещает их собственно в окружение. Такая штука, правда, больше про разработку, а не сервер.
baldr Автор
21.03.2018 19:49Вот про командную строку уж лучше бы вы не писали, право слово. Я даже не приводил это в аргументах, ибо «ps eww » сразу всех сдаст. Да и, серьезно, в командную строку передавать over 20 параметров?
Текущий сеанс — еще лучше… Даже при разработке — если у меня на проект 5 окон терминала открыто, как мне с этим управляться?
В итоге все равно пишем в файл, просто по другому оформленный.marazmiki
21.03.2018 20:05Ну вопрос-то как был задан: как в окружение попадают данные. Как спросили, так и ответил. Ну, точнее, как понял вопрос, так и ответил.
Да и, серьезно, в командную строку передавать over 20 параметров?
Нет никаких шеллов. Не нужны :)
В итоге все равно пишем в файл, просто по другому оформленный.
Ну да. При этом я не вижу ничего плохого в том, чтобы хранить эти переменные в
.service
, при условии защиты последних, конечно.
andreymal
21.03.2018 19:59Позвольте полюбопытствовать, а чем
--env-fle=/path/to/env
принципиально отличается от условного--something=/path/to/local_settings.yaml
?)marazmiki
21.03.2018 20:11Принципиально, наверное, ничем. Не считая того, что
local_settings.yaml
каждый пишет так, как удобно ему, а.env
— признанный индустрией формат, который принимают такие монстры, как docker, systemd, heroku и куча сошек поменьше.
Хотя нет, есть принципиальное отличие.
local_settings.yaml
обязан быть, а.env
— нет. Это лишь один из удобных девелоперу способов указать переменные окружения. На продакшн так стараются не делать, конечно.
У меня такого файла вообще нет, например.
baldr Автор
21.03.2018 18:32Уже стало хорошей традицией записывать все кренделя в виде DSN
Я, было, в комментарии сначала написал что DATABASES не более чем пример, что DB URI удобнее конечно же. Но потом стер, думал и так понятно :)
Я тоже могу спросить что красивее выглядит:
AWS_API_KEY = env('QWERTY', default=None) AWS_API_SECRET = env('ZXCVB', default=None) AWS_BUCKET_NAME = env('mybucket', default=None)
Или:
AWS: API_KEY: QWERTY SECRET: ZXCVB BUCKET: mybucket
И вопрос — какое конкретно значение сейчас на запущенном сервисе на сервере?
DEBUG = env('DEBUG', default=False)
SECRET_KEY = env('SECRET_KEY_FROM_MY_ENV_VAR')
а теперь вас пригласили разобраться с проблемой на сервере. Вы заходите по ssh и видите запущенный какой-то сервис. Его раньше поддерживал какой-то разработчик, но сейчас его нет.
Вроде есть README, в нем инструкция как запускать, какие переменные окружения ставить. В логах совершенно не то что ожидается. Вы останавливаете, затем снова запускаете с теми же (наверное?) переменными. Все работает как надо. Теперь остается гадать — а с ЧЕМ было оно запущено до этого? Возможно сервис запустили вручную с переменными для дебага? А хз теперь.
Это реальный пример из жизни, года два назад еще.
Когда все в одном (ок, пусть даже в нескольких) файлах — мне кажется что все-таки удобнее их контролировать, чем собирать по разным местам.
Из последнего примера — возможный контраргумент — можно запустить сервис, а потом поменять конфиг. Но согласитесь, это гораздо меньше вероятность сделать случайно чем запустить сервис с другими переменными окружения.marazmiki
21.03.2018 18:51На мой взгляд, пример с энвами более красивый. Даже не потому что там энвы, он был бы красивее даже с хардкодом. Из тех хотя бы соображений, что мы определили какие-то переменные сами и знаем, что ничего неожиданного там нет. и никто случайно или по злому умыслу не втыкнул
DEBUG = True
на продакшн.
И вопрос — какое конкретно значение сейчас на запущенном сервисе на сервере?
На такие вопросы отвечают 12 факторов. И, что немаловажно, отвечают и за свои слова :)
И не бывает таких ситуаций, когда кто-то внезапно поменял конфиг и это не сказалось на запущенном приложении.
baldr Автор
21.03.2018 18:56Ок, это уже вопрос вкусовщины, я так и знал что все может свестись к холивару.
Последний вопрос: имея приложение джанги как мне запустить консольный отладчик (`manage.py shell_plus`) с теми же параметрами, с которыми стартует приложение? А еще миграции и остальные команды?marazmiki
21.03.2018 19:14И на этот вопрос тоже отвечают 12 факторов.
Вкратце, идея такая: вводится понятие "ревизия приложения". Ревизия приложения состоит из ревизии кода (натуральной ревизии, из гита которая), списка зависимостей и набора переменных окружения.
Когда нужно задеплоиться: в случае, если появился новый код или поменялось окружение (появилась \ изменилась \ удалилась переменная), создаётся новая ревизия приложения. Технически — это создаётся новый образ, в который помещается кодовая ветка, устанавливаются с нуля все зависимости. И переменные.
Далее на основе этого образа запускается контейнер. И неважно что там:
gunicorn
,./manage.py shell_plus
,./manage.py migrate
— образ один, набор переменных один. После завершения процесса контейнер умрёт.baldr Автор
21.03.2018 19:36Простите, я не могу вам ответить. Как только я скажу что-то против контейнеров, сразу налетит толпа и растопчет тут все, я к такому не готов.
Контейнеры — это неплохо, но не всегда есть прямой смысл их использовать.
Я не вижу необходимости менять у себя сложившуюся практику и считаю что она логична и работает хорошо. Я не пишу ультимативных призывов делать именно так. Количество добавивших статью в закладки говорит о том, что многим что-то показалось полезным.
Мне кажется мы в этой ветке достаточно обсудили преимущества и недостатки переменных и конфигов.marazmiki
21.03.2018 19:5812 факторов — это не конкретная имплементация, это подход. И уж тем более не про контейнеры. Её можно реализовать и на одной машине, хоть и сложнее изоляцию организовать, наверное.
Статья Ваша, безусловно, полезна. Хотя бы вот этим холиваром :) к тому же, я вот узнал, что
systemd
умеет вEnvironmentFile
. Вот реально этого не знал
Понятно, что у каждого опыт свой, но у меня в голове уже вряд ли уложится, как это так можно — вручную заходить по ssh на сервер, чего-то там править. Как можно не использовать контейнеры. Как можно деплоить приложения, не имея гарантии, что они запустятся в том окружении, которое я указал.
Так что давайте и вправду закроем эту тему :)
- Логи писать исключительно в
robert_ayrapetyan
>демонизирует его сам systemd, он же и следит за перезапуском при падениях.
И часто падает?
Для сложных проектов с кучей конфигов рекомендую заводить отдельный проект, в нем уже хранить все конфиги (допы к системным, и самого приложения), скрипты инициализации сервера с нуля и собственно CI\CD. Ну и к самому репозиторию такого проекта доступ только у админа системы.
baldr Автор
Ну бывает, конечно. Например в одном из потоков выбрасывается исключение, что приводит к падению всего приложения. Или asyncio точно так же может что-то выкинуть и завалить сразу все.
Про конфиги в отдельном репозитории — согласен, это вариант, но, конечно же, с сервера доступа к этому репозиторию доступа быть не должно.