В этой статье я хочу поделиться несколькими удобными способами организации вашего проекта на рабочем (даже продакшен) сервере.


Я работаю, в основном, с 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)


  1. robert_ayrapetyan
    20.03.2018 03:13

    >демонизирует его сам systemd, он же и следит за перезапуском при падениях.
    И часто падает?

    Для сложных проектов с кучей конфигов рекомендую заводить отдельный проект, в нем уже хранить все конфиги (допы к системным, и самого приложения), скрипты инициализации сервера с нуля и собственно CI\CD. Ну и к самому репозиторию такого проекта доступ только у админа системы.


    1. baldr Автор
      20.03.2018 16:19

      И часто падает?

      Ну бывает, конечно. Например в одном из потоков выбрасывается исключение, что приводит к падению всего приложения. Или asyncio точно так же может что-то выкинуть и завалить сразу все.
      Про конфиги в отдельном репозитории — согласен, это вариант, но, конечно же, с сервера доступа к этому репозиторию доступа быть не должно.


  1. ebt
    20.03.2018 03:45

    Пожалуйста, поясните, почему вам не подходит supervisord.


    1. markoni
      20.03.2018 12:45
      -1

      Видимо, потому, что supervisord не подходит к python3. Во всяком случае, какое-то время не подходил (какие-то костыли там пилили, да), поэтому мной, например, был отправлен на свалку.


    1. banzayats
      20.03.2018 12:51

      IMHO, зачем нужна еще одна сущность в виде supervisord, если все можно решить исползуя возможности systemd?
      Ну и автор написал, что "если у вас нет systemd, то можно смотреть в сторону supervisord".


    1. skorpix
      20.03.2018 16:20
      +1

      А зачем использовать сторонние решения, если все есть из коробки? Тем более systemd вроде стабильный. Если у вас есть какие-то плюсы supervisord по сравнению с systemd или специфичные случаи использования, поделитесь, мне интересно. Мне для Django+Celery хватает systemd за глаза.


    1. baldr Автор
      20.03.2018 16:22

      Собственно, уже ответили. supervisord — это еще одна программа, приносящая свои собственные баги. Ждать что она будет надежнее чем системный сервис смысла нет, да и нет у него того что не может systemd.


  1. EgorLyutov
    20.03.2018 09:35

    Если уж деплоить приложение без докера, то лучше тогда уж использовать какой-нибудь ansible проект, который разворачивает проекты. Секреты хранить с помощью ansible vault, например. Если используется gitlab и местный CI, то так же загружать можно в настройки CI секреты. Я последнее время использую три окружения для проектов: dev, staging, prod. И уже в зависимости от этой переменной окружения строится окончательный settings.py. То есть мне не нужно заходить на сервер и править там конфиги вручную. Если проект небольшой, то просто docker-compose и на сервере .env файл со всеми переменными.


  1. boo_v2
    20.03.2018 10:41

    Проблема: можно случайно закоммитить файл
    .gitignore


  1. SirEdvin
    20.03.2018 12:24

    Используйте сервис logrotate. Ниже приводится простой конфиг для логов celery.

    Почему не journald, если у вас уже есть systemd? Просто пишете в stdout и профит. И ротация из коробки.


    переменные окружения. Тоже не очень безопасно, не очень удобно (при soft-reload например)

    Эм… что? У вас же systemd, там есть EnvironmentFile, очень удобно. И особой разницы между local.py и этим решением с точки зрения безопасности нет. Если хотите безопасности, вперед, в vault.


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


    1. baldr Автор
      20.03.2018 16:39

      Насчет journald — да, это хорошая идея, спасибо. Надо бы почитать доки и попробовать.
      Однако не очень кроссплатформенно. Если приложение пишет в файл, то это будет работать везде, а уж что делать с логами — можно решить системными инструментами (logrotate).

      Про локальные параметры я написал что это только моя рекомендация. Каждый делает как удобно в его проекте.
      Я встречал совершенно разные подходы — и settings_ivanov.py / settings_stage.py в репозитории, и замену settings.py на нужный при переустановке.
      Меня учили отделять данные от кода и я считаю что это правильно.
      Переменные окружения легко не перечитаешь без перезапуска, в отличие от конфига. Да и при запуске любого дочернего процесса он наследует все переменные, что не всегда хорошо. Мне это не нравится и я не использую.


      1. SirEdvin
        20.03.2018 16:44

        Однако не очень кроссплатформенно.

        Кроссплатформенно, так как вы пишите в stdout. А обрабатывать stdout можно уже инструментами платформы, в нашем случае это journald. На винде есть какие-то другие штуки. Ну и более того, вполне можно предположить, что ваше приложение все равно не кросс-платформенно.


        Меня учили отделять данные от кода и я считаю что это правильно.

        Простите, какие данные? Вы предлагаете по не совсем ясным причинам отделять настройки приложения от самого приложения. Какой профит вы от этого получаете? Минусы я знаю, они заключаются в том, что надо каждый раз дергать человека, который занимается деплоем, что бы он правил файл настроек.


        1. baldr Автор
          20.03.2018 17:05

          можно предположить, что ваше приложение все равно не кросс-платформенно.

          Предположение не очень обоснованно. У меня есть кросс-платформенные проекты. На windows запускается django-приложение под апачем через WSGI и вот чего я точно не хочу, так это логов приложения в логах апача.
          На мой взгляд, модуль logging отлично справляется с выводом в файлы (разные) и эти файлы удобно обрабатывать (чем-то сторонним), но смешивать я не советую.

          Простите, какие данные?

          Настройки — это данные.
          У меня идея следующая: в папке проекта не должно быть никаких файлов, которые изменяются в период между обновлениями версии. Код — отдельно, данные, логи, временные файлы, конфиги — в своих папках.
          Далеко не всегда получается так что весь проект написан на одном языке и чтение параметров из единого json/yaml/cfg файла удобнее.


          1. SirEdvin
            20.03.2018 18:56

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

            Эм… а код? Стоит заметить, что очень часто код и настройки меняются одновременно, потому что добавляется новый функционал, который как бы нужно настраивать.


            Далеко не всегда получается так что весь проект написан на одном языке и чтение параметров из единого json/yaml/cfg файла удобнее.

            Эм… это как у вас так получается проект на разных языках? Вызов скриптов на других языках?


            1. baldr Автор
              20.03.2018 21:14

              Тем не менее, код — сам по себе, настройки — отдельно. Если вы посмотрите в файловую систему Linux, вы примерно таким образом организованные файлы и найдете, это не я придумал.

              Эм… это как у вас так получается проект на разных языках? Вызов скриптов на других языках?

              Ну а что такого? Чем меньше языков, тем проще, конечно, но не всегда так получается. Особенно когда в проект приходят новые творческие участники.


              1. SirEdvin
                20.03.2018 23:16

                Тем не менее, код — сам по себе, настройки — отдельно. Если вы посмотрите в файловую систему Linux, вы примерно таким образом организованные файлы и найдете, это не я придумал.

                Да, и поэтому каждый пакет тянет с собой настройки или же приложение заполняет дефолтные настройки. А потом еще надо при баг-репортах добавлять конфиги, иначе ничего не понятно.
                Но у вас то окружение, которое вы контролируете, можно позволить себе больше в угоду удобству.


  1. n_elloco
    20.03.2018 16:47
    +1

    Для загрузки параметров можно использовать вот такой инструмент


  1. bykvaadm
    20.03.2018 17:00
    -1

    ну, вот поэтому вы и воротите всякую фигню, потому что в докер не умеете =)


    1. baldr Автор
      20.03.2018 17:07

      Ага, докер, блокчейн, devops, go и еще несколько модных слов в каждом проекте помогают сохранить лицо перед начальством и добавить уверенности в отчеты инвесторам.


      1. jetexe
        20.03.2018 17:53

        А так же масштабировать приложение*
        *кроме блокчейна, он не помогает.


      1. SirEdvin
        20.03.2018 19:04

        Да, гораздо лучше настраивать все на сервере исключительно ручками и там хранить настройки. Когда вы один, можно вообще делать любую дичь и все будет отлично, а вот как только начинается нормальная работа, то вот тут приходится делать по нормальному. И так уж получилось, что контейнеризация (в том числе и докер) заставляет задуматься о том, что такое нормально немного раньше.


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


      1. viiy
        20.03.2018 19:53

        :sarcasm:
        Вы забыли kubernates (какой же докер без него?)
        И aws. Тогда уж и terraform.
        gitlab, ansible
        Плюс consul, vault
        Что еще модно… ceph как block storage
        Мониторинг, прометеус, elk для логов
        Своя автономная зона, cdn, сеть фильтрации трафика и ddos
        Кавку куданибудь там сбоку надо обязательно, как вишенку на торт


      1. ahmpro
        21.03.2018 01:55

        зачем бросаться в крайности? можно просто стараться следовать 12factor.net


  1. vaxXxa
    20.03.2018 18:22

    Где вы храните логи и как вы их просматриваете? journalctl?


  1. Closius
    20.03.2018 18:59
    +1

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


    1. SirEdvin
      20.03.2018 19:13

      Для любых проектов нужен ansible или другая система. Я понимаю предупреждения против докера, особенно на мелких проектах, но если вы не используете ansible или альтернативы — вы делаете неправильно.


      1. Closius
        20.03.2018 19:17

        А как же люди жили до Ансибля (он появился в 2012)?


        1. SirEdvin
          20.03.2018 19:21
          +1

          Был, например, CFEngine. А потом еще Chef и puppet. До него жили на самописных скриптах и жили плохо, да.


          А еще люди умирали 1000 лет назад в 30 лет в том числе от антисанитарии. Будете ли иронизировать о том, как же люди жили раньше без мыла?


          У вас будут реальные аргументы, почему не стоит использовать ansible на мелких проектах, кроме "Ой, сложна"? Поверьте, оверхед по времени ближе к середине проекта выравнивается, а если вы уже имеете опыт его использования, то самописные скрипты требуют значительно больше времени на кастомизацию.


          1. Closius
            20.03.2018 19:33
            +1

            Ну так знаете можно дохрена чего нагородить в маленький проект. И докер и систему мониторинга и риал тайм мониторинг с отправкой сообщений на приватный телеграм канал… Все зависит от потребностей. Я ни в коем случае не говорю, что ансибль плохо. Просто не вижу смысл ставить его приоритетным для маленького проекта, как минимум ансибль это дополнительная сущность, а чем меньше сущностей используешь, тем надежнее. Когда уже будет понятно, что проект будет разрастаться, можно и ансибль прикрутить. Научиться использовать его конечно будет не лишним.
            У меня бывало проекты умещались меньше чем на 50 строк баш скрипта, нафиг мне тут ансибль?


            1. SirEdvin
              20.03.2018 19:39

              Вы пишете доку по тому, как развернуть ваш проект? Вот ansible это дока, только которая еще и выполняется.
              В добавок к этому размер ansible скриптов всегда четко следует размерам проекта или связанной инфраструктуры. Если у вас просто баш скрипт, то вы или выгружаете его на сервер через ansible или разворачиваете там git репозиторий с проектом (и ключи для него). И все, это делается довольно быстро.


              Ну и "а чем меньше сущностей используешь, тем надежнее", подразумевает, что вы не будете делать то, что делает ansible, но нет, у вас все равно будут самописные скрипты по деплою. И вместо того, что бы использовать продукт, который для этого разрабатывался вы предлагаете на коленке напилить ad-hoc решение, у которого не будет никаких преимуществ. Зачем?


  1. andreymal
    20.03.2018 19:41
    +1

    sudo ln -s /opt/myservice/config/systemd/*.service /etc/systemd/system/

    Взламываем сайт, залезаем в файл /opt/myservice/config/systemd/*.service (который на запись естественно никто не закрыл), меняем в файле User=myuser на User=root и ExecStart по вкусу — опа, рут успешно получен хацкером.


    1. baldr Автор
      20.03.2018 21:18

      Хороший пример, спасибо. Да, наверное тогда правильнее будет скопировать вместо симлинков и поставить права?


      1. andreymal
        20.03.2018 21:22

        Вроде как сервисы в systemd вообще может создавать сам пользователь без вмешательства рута (где-то в ~/.config/systemd если верить арчевики), но я тут не спец, сам я по старой привычке юзаю supervisord с копированием конфигов)


  1. rusnasonov
    20.03.2018 21:18
    +1

    Еще бы добавил в важные моменты логирования — copytruncate в logrotate сначала делает копию, а потом урезает файл. Все строки, которые будут записаны в лог после копирования но перед урезанием успешно потеряются.


    1. baldr Автор
      20.03.2018 21:32

      Да, к сожалению, вы правы. Спасибо, добавил


  1. marazmiki
    21.03.2018 06:42

    Вот Вы утверждаете, что supervisord — лишняя сущность и надо её избегать. Но при этом рекомендуете файлы с логами ротейтить, а конфиги хранить не в переменных окружения, а в файлах. Да ещё и велосипед какой-то для этого придумали. На мой взгляд, это двойные стандарты :)


    Если уж избавляться от лишних сущностей, то по-большому. Тезисно:


    1. Логи писать исключительно в stdout;
    2. Если есть systemd, использовать его. Если нет, обновлять сервер, чтобы был. Или менять на другой. Ни в коему случае не использовать supervisord сотоварищи.
    3. Контейнеры по возможности использовать, причём с readonly-файловой системой.
    4. Хранить настройки в переменных окружения

    Здесь, напоминаю, только тезисы. Давайте более подробно обсудим в комментариях?


    P.S. Хабраюзер ahmpro уже упоминал про 12 факторов. Рекомендую обратить внимание и почитать. Соглашаться со всем не обязательно, но крайне полезно для знакомства.


    1. marazmiki
      21.03.2018 06:55

      Почему не надо писать логи в файл


      • Потому что писать на жёсткий диск долго;
      • Потому что запись в файловую систему привязывает нас к собственно файловой системе.
      • Потому что приложение обязано сообщать о произошедших событиях, а не сохранять их.

      Почему надо писать логи в STDOUT


      • Потому что это стандартный вывод. Он есть везде и везде более-менее одинаково работает
      • Потому что стандартным выводом процесса может управлять файловая система (или пользователь). Этот стандартный вывод можно грепать в реальном времени; его можно писать в journald, его можно отправлять в fluentd, его даже можно писать в файл и логротейтить :)
        Но заниматься этим должно не наше приложение.


    1. marazmiki
      21.03.2018 07:01

      Почему не нужно использовать supervisord


      • Потому что внешняя зависимость, без которой можно обойтись
      • Потому что глючный
      • Потому что python2

      Ещё, помнится, был проект circus. Что-то типа supervisord, но для Python3. В бою не испытывал, да и смысла особого нет, поскольку, согласно бритве Оккама, лучше пользоваться тем, что есть.


      1. andreymal
        21.03.2018 10:37

        Потому что глючный

        Как активно пользующий supervisord заявляю, что нет


        Потому что python2

        Как кодящий на обоих питонах заявляю, что ничего плохого в этом нет


        (с первым пунктом не спорю)


        1. marazmiki
          21.03.2018 11:43

          Я буквально на днях переносил старую кодовую базу на новое железо и старался использовать тот же софт, что и раньше. Так вот, как supervisord не умел 4 года назад убивать воркеры celery при перезапуске задачи, так и не умеет до сих пор. В принципе, этого одного уже достаточно, чтобы им не пользоваться :-)


          1. andreymal
            21.03.2018 11:45

            Зачем supervisord'у убивать воркеры celery? Ему нужно убить лишь мастер-процесс, а мастер-процесс должен убить всех своих воркеров самостоятельно — именно так у меня работает celery внутри supervisord уже несколько лет, всё прекрасно и не глючит.


            1. marazmiki
              21.03.2018 11:54

              Виноват, а воркер целери не может быть мастер-процессом?


              1. andreymal
                21.03.2018 12:01

                Я не разбирался в деталях, как работает celery и что он может, но я его сейчас запускаю как celery -A myapp worker --concurrency=2 -B и это запускает четыре процесса: мастер, воркер, воркер и celerybeat. Когда я отправляю SIGINT мастер-процессу, остальные три процесса автоматически завершаются следом за ним.


    1. marazmiki
      21.03.2018 07:04

      Почему лучше использовать контейнеры.


      • Потому что если выломают приложение, пострадает только контейнер, а на хост-система
      • Потому что можно добиться идентичного окружения в на бою и на машине разработчика, даже если на сервере Linux, а у разработчика — macos (или windows, ни к ночи упомянута будет).
      • Разработка в контернере с файловой системой, примонтированной в ReadOnly дисциплинирует: приходится писать так, чтобы не привязываться к собственно файловой системе. Поэтому приложения получаются такими, что их легче масштабировать
      • Это, чёрт возьми, модно!


      1. andreymal
        21.03.2018 10:43

        Потому что если выломают приложение, пострадает только контейнер, а на хост-система

        Если выломают приложение, то пострадает только приложение, если руки не кривы нужные юниксовые права правильно настроить


        Потому что можно добиться идентичного окружения

        Идентичное окружение — зло. Хорошо сделанное приложение должно одинаково хорошо работать не а каком-то ОДНОМ, а а ЛЮБОМ окружении — от фряхи на большом сервере до малинки в ближайшей кофеварке. Если разработчик не в состоянии обеспечить работу приложения за пределами одного окружения, это указывает на кривизну его рук, а контейнеры лишь прикроют говнокод в данном случае. (Есть, конечно, редкие исключения, когда делать работоспособность в любом окружении слишком геморройно, но например обычные веб-сайты абсолютно точно не в их числе.)


        Контейнер всё стерпит, ага


        1. ahmpro
          21.03.2018 11:30

          Под идентичное окружением имелся ввиду один и тот же набор (с совпадением вплоть до минорных версий) библиотек и системных зависимостей, необходимых для работы приложения, речи про где запускается код в данном случае не идет.

          Сейчас все стараются изолировать код от системного окружения где это запускается (12 факторов этому способствует), то на что вы собственно и обратили внимание.
          Контейнеры лишь удобный механизм сделать это с минимальными трудозатратами, сводя к минимуму необходимость разработчику думать о том, где его код будет запущен.
          То, что некоторые юзают контейнеры по старинке, запихивая всякую фигню в них, то дело в некомпетентности, а не в самих контейнерах.

          Это, чёрт возьми, модно!


          Мода лишь следствие удобства, которое контейнеры дают. Да, не бесплатно, но кто говорил, что это серебряная пуля? :D


          1. andreymal
            21.03.2018 11:35

            Под идентичное окружением имелся ввиду один и тот же набор (с совпадением вплоть до минорных версий) библиотек и системных зависимостей

            Если нужно воспроизвести баг, который в других условиях не воспроизводится, то это хорошо. Но опять же затачиваться целиком на одну конкретную версию плохо: хорошо сделанное приложение должно работать в пределе на ЛЮБЫХ версиях зависимых библиотек, тем более в пределах одной мажорной версии (суровая реальность, конечно, накладывает ограничения из-за всяких там багов и обратной совместимости, но стремиться к поддержке по возможности больше числа версий нужно). Идеальное по моему мнению приложение должно работать где угодно от устаревшей центоси до самой свежей убунты, если нет уважительных причин для иного (лень разработчиков — не уважительная причина).


            1. ahmpro
              21.03.2018 12:10

              Идеальное приложение не имеет зависимостей и вообще написанного кода, так что давайте без фантазий :D


        1. marazmiki
          21.03.2018 12:04
          +1

          Если выломают приложение, то пострадает только приложение, если руки не кривы нужные юниксовые права правильно настроить

          А вдруг кривые. Зачем оставлять потенциальную возможность?


          Идентичное окружение — зло.

          Голословное утверждение. И в корне неверное. Хорошее приложение должно делать только одно: выполнять поставленную бизнес-задачу с разумными затратами русурсов (времени, денег, мощностей). Всё остальное — пожелания, причём не всегда критичные.


          Даже если в контейнере говнокод — да ради бога, если он работает, не просит каши и его можно поддерживать. Действительно, контейнер всё стерпит. Хорошее правило.


          Требование же чтобы работало везде… А зачем?


          1. andreymal
            21.03.2018 12:07
            -2

            А вдруг кривые

            Тогда и контейнер не поможет :D


            Действительно, контейнер всё стерпит. Хорошее правило. Требование же чтобы работало везде… А зачем?

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


            1. marazmiki
              21.03.2018 12:22

              Если руки кривые (а это бывает… ох бывает), то контейнер кокрастыке поможет.


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

              Из-за какого мнения? Программирование — это не искусство, это прикладная область, за неё кушать дают. А хочется прекрасного — участвуйте в олимпиадах. Денег не заплатят, ну так художник и должен быть голодным :)


              1. andreymal
                21.03.2018 12:26

                Программирование — это не искусство

                Вот примерно из-за такого мнения и хочу свалить :)


                А хочется прекрасного — участвуйте в олимпиадах

                А я участвовал, в них обычно как раз лютейший фигак-фигак-и-в-продакшен)


      1. andreymal
        21.03.2018 11:17

        даже если на сервере Linux, а у разработчика — macos

        А отличная от линукса ОС, кстати, автоматически означает необходимость использования виртуальной машины (Docker на макоси использует VirtualBox, а на винде Hyper-V, если я правильно понял его документацию). Но виртуалка по сути ведь является контейнером тоже — зачем пихать контейнер внутрь контейнера? Можно выкатить ansible-плейбук на убунточку, запихнутую в виртуалбокс, и получить идентичное продакшену окружение — я для своих сайтов делаю именно так.


        1. ahmpro
          21.03.2018 11:34

          vagrant, provisioning энсимблом и автоматический накат дампов с лайва(или транка) просто удобнейший способ локальной разработки, для нового разработчика это пара команд:
          git clone
          vagrant up
          ./setup.sh (внутри виртуалки, если проект достаточно сложный и требуется донастройка, которая тоже автоматическая и заранее написана, используя теже ansible-роли)


    1. marazmiki
      21.03.2018 07:21

      Про хранение настроек


      На мой взгляд, среди всех перечисленных способов Вы порекомендовали самый неудачный. По сути, он имеет все те же самые минусы, что и local_settings.py, кроме одного: его случайно не поместить в репозиторий. Да и то плюс этот сомнительный, с учётом .gitignore


      Зато привносит дополнительные минусы:


      • Магия. Сами упомянули.
      • Лишняя зависимость. Не столько даже yaml, сколько необходимость размещать этот "магический" код в каком-то файле, чтобы его импортировать
      • Захардкоженные пути. По идее, они должны быть одинаковы, но где, к примеру, искать /usr/local/etc/ в Windows? :) Или логику городить? Тогда почему бы просто не остановиться на local_settings?

      Переменные окружения таких проблем не продуцируют.


      • Они специально придуманы для конфигурации окружения, в котором запускается процесс. Конфигурация от народа!
      • Их поддержка есть в каждой операционной системе
      • Их использование всегда одинаково и на windows, и в macos, и в linux, и в докер-контейнерах.
      • Они случайно не попадут в версионный контроль
      • Они прекрасно интегрируются с тем же systemd

      Про какие неудобства идёт речь — я так и не понял. И почему они менее защищены чем файл, лежащий снаружи репозитрия.


      И вот этот Ваш коммментарий:


      Переменные окружения легко не перечитаешь без перезапуска, в отличие от конфига. Да и при запуске любого дочернего процесса он наследует все переменные, что не всегда хорошо. Мне это не нравится и я не использую.

      тоже не понял. Особенно в контексте джанги выглядит странно, где конфиг вычисляется при старте приложения.


      1. baldr Автор
        21.03.2018 15:48

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

        тоже не понял. Особенно в контексте джанги выглядит странно, где конфиг вычисляется при старте приложения.

        Ок, давайте возьмем тот же gunicorn с его child-процессами. Если вы подложили новый код (или новые настройки даже) и выполнили «kill -HUP», то он аккуратно перезапустит воркеры и они начнут выполняться уже с новым кодом, трафик потерян не будет. Однако переменные окружения будут те же самые, что и при старте мастера. И поменять их у мастера вы не сможете без рестарта процесса.


        1. marazmiki
          21.03.2018 16:16

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

          Других аргументов, извините, не увидел. И по старинному правилу «бремя доказательства лежит на обвиняющем», прошу всё же показать аргументы :)


          1. baldr Автор
            21.03.2018 17:10
            +1

            Ну еще пример с переменными окружения там где дочерние процессы наследуют окружение родителя. Запустили вы в subprocess какой-нибудь внешний процесс, а он получил все ваши пароли, а потом в крэшдампе отправил своим разработчикам.
            В сети бродят страшилки про «ps -eww », но у меня не получилось повторить.
            Можно привести из пальца высосанный вектор атаки когда, получив доступ к выполнению кода с непривилегированным пользователем можно попробовать почитать все файлы в /proc/<все pidы>/environ (для процессов из-под этого пользователя), но слишком маловероятна удача.
            Все перечисленные в этом комменте проблемы лечатся запуском из-под другого пользователя и очисткой переменных окружения перед запуском, но все ли так делают?

            Я реально не вижу удобства в переменных окружения. Мне лично удобнее прочитать переменную DATABASES как dict из yaml/json файла чем составлять ее из нескольких переменных окружения.


            1. 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')

              ? Мы же помним, что явное лучше неявного. Кто его знает, какие переменные понаинжектили злобные русские хакеры в конфиг, который Вы инклюдите? :)


              1. andreymal
                21.03.2018 18:24

                У меня тут на одном моём сайте два десятка настроек только из тех, что переопределены для боевого окружения, плюс при желании можно насчитать ещё полсотни-сотню настроек со значениями по умолчанию. Лично мне запихивать это всё в переменные окружения было бы не очень удобно)


                1. marazmiki
                  21.03.2018 18:34

                  Ну да, засунуть эти оверрайды в отдельный файл — гораздо более продуманная тактика :)


                  1. baldr Автор
                    21.03.2018 18:36

                    А как бы вы сделали с переменными окружения? Куда бы вы их засунули?


                    1. marazmiki
                      21.03.2018 19:21

                      Куда бы я засунул переменые окружения? Сложный вопрос. Наверное, я бы их и оставил в переменных окружения :)


                      1. andreymal
                        21.03.2018 19:23

                        Но переменные окружения вроде ведь не берутся сами по себе из воздуха? Откуда они появляются?


                        1. marazmiki
                          21.03.2018 19:26

                          Их выставляет пользователь. Или сама система. Или их передаёт процесс-родитель. Возможны варианты, короче. Я суть вопроса, признаться, не уловил


                          1. andreymal
                            21.03.2018 19:29

                            Выставляет пользователь где и как? Процесс-родитель у себя их откуда берёт? Какие именно варианты возможны?


                            Я суть вопроса, признаться, не уловил

                            Вот у меня двадцать условных параметров для сайта и некий ваш условный контейнер, например — как конкретно мне запустить «неважно что там: gunicorn, ./manage.py shell_plus, ./manage.py migrate» с этими вот двадцатью параметрами с использованием этих ваших переменных окружения?


                            1. baldr Автор
                              21.03.2018 19:38

                              Да они точно так же пишут их в файл манифеста для докера, только добавляют там еще больше настроек для самого докера.


                            1. 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 в текущей директории. И если есть, считывает переменные и помещает их собственно в окружение. Такая штука, правда, больше про разработку, а не сервер.


                              1. baldr Автор
                                21.03.2018 19:49

                                Вот про командную строку уж лучше бы вы не писали, право слово. Я даже не приводил это в аргументах, ибо «ps eww » сразу всех сдаст. Да и, серьезно, в командную строку передавать over 20 параметров?

                                Текущий сеанс — еще лучше… Даже при разработке — если у меня на проект 5 окон терминала открыто, как мне с этим управляться?

                                В итоге все равно пишем в файл, просто по другому оформленный.


                                1. marazmiki
                                  21.03.2018 20:05

                                  Ну вопрос-то как был задан: как в окружение попадают данные. Как спросили, так и ответил. Ну, точнее, как понял вопрос, так и ответил.


                                  Да и, серьезно, в командную строку передавать over 20 параметров?

                                  Нет никаких шеллов. Не нужны :)


                                  В итоге все равно пишем в файл, просто по другому оформленный.

                                  Ну да. При этом я не вижу ничего плохого в том, чтобы хранить эти переменные в .service, при условии защиты последних, конечно.


                              1. andreymal
                                21.03.2018 19:59

                                Позвольте полюбопытствовать, а чем --env-fle=/path/to/env принципиально отличается от условного --something=/path/to/local_settings.yaml?)


                                1. marazmiki
                                  21.03.2018 20:11

                                  Принципиально, наверное, ничем. Не считая того, что local_settings.yaml каждый пишет так, как удобно ему, а .env — признанный индустрией формат, который принимают такие монстры, как docker, systemd, heroku и куча сошек поменьше.


                                  Хотя нет, есть принципиальное отличие. local_settings.yaml обязан быть, а .env — нет. Это лишь один из удобных девелоперу способов указать переменные окружения. На продакшн так стараются не делать, конечно.


                                  У меня такого файла вообще нет, например.


              1. 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, в нем инструкция как запускать, какие переменные окружения ставить. В логах совершенно не то что ожидается. Вы останавливаете, затем снова запускаете с теми же (наверное?) переменными. Все работает как надо. Теперь остается гадать — а с ЧЕМ было оно запущено до этого? Возможно сервис запустили вручную с переменными для дебага? А хз теперь.
                Это реальный пример из жизни, года два назад еще.

                Когда все в одном (ок, пусть даже в нескольких) файлах — мне кажется что все-таки удобнее их контролировать, чем собирать по разным местам.
                Из последнего примера — возможный контраргумент — можно запустить сервис, а потом поменять конфиг. Но согласитесь, это гораздо меньше вероятность сделать случайно чем запустить сервис с другими переменными окружения.


                1. marazmiki
                  21.03.2018 18:51

                  На мой взгляд, пример с энвами более красивый. Даже не потому что там энвы, он был бы красивее даже с хардкодом. Из тех хотя бы соображений, что мы определили какие-то переменные сами и знаем, что ничего неожиданного там нет. и никто случайно или по злому умыслу не втыкнул DEBUG = True на продакшн.


                  И вопрос — какое конкретно значение сейчас на запущенном сервисе на сервере?

                  На такие вопросы отвечают 12 факторов. И, что немаловажно, отвечают и за свои слова :)


                  И не бывает таких ситуаций, когда кто-то внезапно поменял конфиг и это не сказалось на запущенном приложении.


                  1. baldr Автор
                    21.03.2018 18:56

                    Ок, это уже вопрос вкусовщины, я так и знал что все может свестись к холивару.
                    Последний вопрос: имея приложение джанги как мне запустить консольный отладчик (`manage.py shell_plus`) с теми же параметрами, с которыми стартует приложение? А еще миграции и остальные команды?


                    1. marazmiki
                      21.03.2018 19:14

                      И на этот вопрос тоже отвечают 12 факторов.


                      Вкратце, идея такая: вводится понятие "ревизия приложения". Ревизия приложения состоит из ревизии кода (натуральной ревизии, из гита которая), списка зависимостей и набора переменных окружения.


                      Когда нужно задеплоиться: в случае, если появился новый код или поменялось окружение (появилась \ изменилась \ удалилась переменная), создаётся новая ревизия приложения. Технически — это создаётся новый образ, в который помещается кодовая ветка, устанавливаются с нуля все зависимости. И переменные.


                      Далее на основе этого образа запускается контейнер. И неважно что там: gunicorn, ./manage.py shell_plus, ./manage.py migrate — образ один, набор переменных один. После завершения процесса контейнер умрёт.


                      1. baldr Автор
                        21.03.2018 19:36

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

                        Мне кажется мы в этой ветке достаточно обсудили преимущества и недостатки переменных и конфигов.


                        1. marazmiki
                          21.03.2018 19:58

                          12 факторов — это не конкретная имплементация, это подход. И уж тем более не про контейнеры. Её можно реализовать и на одной машине, хоть и сложнее изоляцию организовать, наверное.


                          Статья Ваша, безусловно, полезна. Хотя бы вот этим холиваром :) к тому же, я вот узнал, что systemd умеет в EnvironmentFile. Вот реально этого не знал


                          Понятно, что у каждого опыт свой, но у меня в голове уже вряд ли уложится, как это так можно — вручную заходить по ssh на сервер, чего-то там править. Как можно не использовать контейнеры. Как можно деплоить приложения, не имея гарантии, что они запустятся в том окружении, которое я указал.


                          Так что давайте и вправду закроем эту тему :)