- PVSM.RU - https://www.pvsm.ru -
[1]
Так уж вышло, что запуск cron в Docker-контейнере — дело весьма специфическое, если не сказать сложное. В сети полно решений и идей на эту тему. Вот один из самых популярных (и простых) способов запуска:
cron -f
Но такое решение (и большинство других тоже) обладает рядом недостатков, которые сходу обойти достаточно сложно:
Проблему просмотра логов с использованием стандартных средств Docker устранить сравнительно легко. Для этого достаточно принять решение о том, в какой файл будут писать свои логи cron-задания. Предположим, что это /var/log/cron.log:
* * * * * www-data task.sh >> /var/log/cron.log 2>&1
Запуская после этого контейнер при помощи команды:
cron && tail -F /var/log/cron.log
мы всегда сможем видеть результаты выполнения заданий при помощи «docker logs».
Аналогичного эффекта можно добиться воспользовавшись перенаправлением /var/log/cron.log в стандартный вывод контейнера:
ln -sf /dev/stdout /var/log/cron.log
Если cron-задания пишут логи в разные файлы, то, скорее всего, предпочтительнее будет вариант с использованием tail, который может «следить» за несколькими логами одновременно:
cron && tail -F /var/log/task1.log /var/log/task2.log
Изучая информацию на тему назначения переменных окружения для задач cron, выяснил, что последний может использовать так называемые подключаемые модули аутентификации (PAM) [2]. Что на первый взгляд является не относящимся к сабжу теме фактом. Но у PAM есть возможность определять и переопределять любые переменные окружения для служб, которые его (точнее их, модули аутентификации) используют, в том числе и для cron. Вся настройка производится в файле /etc/security/pam_env.conf (в случае Debian/Ubuntu). То есть любая переменная, описанная в этом файле, автоматически попадает в Environment всех cron-заданий.
Но есть одна проблема, точнее даже две. Синтаксис файла (его описание) при первом взгляде может ввести в ступор обескуражить. Вторая проблема — это как при запуске контейнера перенести переменные окружения внутрь pam_env.conf.
Опытные Docker-пользователи насчет второй проблемы наверняка сразу скажут, что можно воспользоваться лайфхаком под названием docker-entrypoint.sh и будут правы. Суть этого лайфхака заключается в написании специального скрипта, запускаемого в момент старта контейнера, и являющегося входной точкой для параметров, перечисленных в CMD или переданных в командной строке. Скрипт можно прописать внутри Dockerfile, например, так:
ENTRYPOINT ["/docker-entrypoint.sh"]
А его код при этом должен быть написан специальным образом:
#!/usr/bin/env bash
set -e
# код переноса переменных окружения в /etc/security/pam_env.conf
exec "$@"
Вернемся к переносу переменных окружения немного позже, а пока остановимся на синтаксисе файла pam_env.conf. При описании любой переменной в этом файле значение можно указать c помощью двух директив: DEFAULT и OVERRIDE. Первая позволяет указать значение переменной по умолчанию (если та вообще не определена в текущем окружении), а вторая позволяет значение переменной переопределить (если значение этой переменной в текущем окружении есть). Помимо этих двух кейсов, в файле в качестве примера описаны более сложные кейсы, но нас по большому счету интересует только DEFAULT. Итого, чтобы определить значение для какой-нибудь переменной окружения, которая затем будет использовать в cron, можно воспользоваться таким примером:
VAR DEFAULT="value"
Обратите внимание на то, что value в данном случае не должно содержать названий переменных (например, $VAR), потому как контекст файла выполняется внутри целевого Environment, где указанные переменные отсутствуют (либо имеют другое значение).
Но можно поступить еще проще (и такой способ почему-то не описан в примерах pam_env.conf). Если вас устраивает, что переменная в целевом Environment будут иметь указанное значение, независимо от того, определена она уже в этом окружении или нет, то вместо вышеупомянутой строки можно записать просто:
VAR="value"
Тут следует предупредить о том, что $PWD, $USER и $PATH вы не сможете заменить для cron-заданий при любом желании, потому как cron назначает значения этих переменных исходя из своих собственных убеждений. Можно, конечно, воспользоваться различными хаками [3], среди которых есть и рабочие, но это уже на ваше усмотрение.
Ну и наконец, если нужно перенести все текущие переменные в окружение cron-заданий, то в этом случае можно использовать такой скрипт:
#!/usr/bin/env bash
set -e
# переносим значения переменных из текущего окружения
env | while read -r LINE; do # читаем результат команды 'env' построчно
# делим строку на две части, используя в качестве разделителя "=" (см. IFS)
IFS="=" read VAR VAL <<< ${LINE}
# удаляем все предыдущие упоминания о переменной, игнорируя код возврата
sed --in-place "/^${VAR}/d" /etc/security/pam_env.conf || true
# добавляем определение новой переменной в конец файла
echo "${VAR} DEFAULT="${VAL}"" >> /etc/security/pam_env.conf
done
exec "$@"
Поместив скрипт «print_env» в папку /etc/cron.d внутри образа и запустив контейнер (см. Dockerfile), мы сможем убедиться в работоспособности этого решения:
* * * * * www-data env >> /var/log/cron.log 2>&1
FROM debian:jessie
RUN apt-get clean && apt-get update && apt-get install -y rsyslog
RUN rm -rf /var/lib/apt/lists/*
RUN touch /var/log/cron.log
&& chown www-data:www-data /var/log/cron.log
COPY docker-entrypoint.sh /
COPY print_env /etc/cron.d
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["/bin/bash", "-c", "cron && tail -f /var/log/cron.log"]
docker build --tag cron_test .
docker run --detach --name cron --env "CUSTOM_ENV=custom_value" cron_test
docker logs -f cron # нужно подождать минуту
Говоря о причине невозможности нормального завершения описанного контейнера с cron, следует упомянуть о способе общения демона Docker с запущенной внутри него службой. Любая такая служба (процесс) запускается с PID=1, и только с этим PID Docker умеет работать. То есть каждый раз, когда Docker посылает управляющий сигнал в контейнер, он адресует его процессу с PID=1. В случае с «docker stop» это SIGTERM и, если процесс продолжает работу, через 10 секунд SIGKILL. Так как для запуска используется "/bin/bash -c" (в случае с «CMD cron && tail -f /var/log/cron.log» Docker все равно использует "/bin/bash -c", просто неявно), то PID=1 получает процесс /bin/bash, а cron и tail уже получают другие PID, предугадать значения которых не представляется возможным по очевидным причинам.
Вот и выходит, что когда мы выполняем команду «docker stop cron» SIGTERM получает процесс "/bin/bash -с", а он в этом режиме игнорирует любой полученный сигнал (кроме SIGKILL, разумеется).
Первая мысль в этом случае обычно — надо как-то «кильнуть» процесс tail. Ну это сделать достаточно легко:
docker exec cron killall -HUP tail
Круто, контейнер тут же прекращает работу. Правда насчет graceful тут есть некоторые сомнения. Да и код ошибки по прежнему отличен от нуля. В общем, я не смог продвинуться в решении проблемы, следуя этим путем.
Кстати, запуск контейнера при помощи команды cron -f также не дает нужного результата, cron в этом случае просто отказывается реагировать на какие-либо сигналы.
Остается только одно — написать отдельный скрипт запуска демона cron, умеющий при этом правильно реагировать на управляющие сигналы. Относительно легко, даже если раньше на bash'е писать не приходилось, можно найти информацию о том, что в нем есть возможность запрограммировать обработку сигналов (при помощи команды trap). Вот как, к примеру, мог бы выглядеть такой скрипт:
#!/usr/bin/env bash
# перенаправляем /var/log/cron.log в стандартный вывод
ln -sf /dev/stdout /var/log/cron.log
# запускаем syslog и cron
service rsyslog start
service cron start
# ловим SIGINT или SIGTERM и выходим
trap "service cron stop; service rsyslog stop; exit" SIGINT SIGTERM
если бы мы могли каким-то образом заставить этот скрипт работать бесконечно (до получения сигнала). И тут на помощь приходит еще один лайфхак, подсмотренный тут [4], а именно — добавление в конец нашего скрипта таких строчек:
# запускаем в фоне процесс "tail -f" и ждем его завершения
tail -f /dev/null & wait $!
Или, если cron-задания пишут логи в разные файлы:
# запускаем в фоне процесс "tail -F" и ждем его завершения
tail -F /var/log/task1.log /var/log/task2.log & wait $!
В итоге получилось эффективное решение для запуска cron внутри Docker-контейнера, обходящее ограничения первого и соблюдающее правила второго, с возможностью нормальной остановки и перезапуска контейнера.
В конце привожу полные листинги Dockerfile и start-cron, которыми я пользуюсь сейчас.
#!/usr/bin/env bash
# переносим значения переменных из текущего окружения
env | while read -r LINE; do # читаем результат команды 'env' построчно
# делим строку на две части, используя в качестве разделителя "=" (см. IFS)
IFS="=" read VAR VAL <<< ${LINE}
# удаляем все предыдущие упоминания о переменной, игнорируя код возврата
sed --in-place "/^${VAR}/d" /etc/security/pam_env.conf || true
# добавляем определение новой переменной в конец файла
echo "${VAR} DEFAULT="${VAL}"" >> /etc/security/pam_env.conf
done
# запускаем syslog и cron
service rsyslog start
service cron start
# ловим SIGINT или SIGTERM и выходим
trap "service cron stop; service rsyslog stop; exit" SIGINT SIGTERM
# запускаем в фоне процесс "tail -f /dev/null" и ждем его завершения
tail -f /dev/null & wait $!
FROM debian:jessie
RUN apt-get clean && apt-get update && apt-get install -y rsyslog
RUN rm -rf /var/lib/apt/lists/*
RUN touch /var/log/cron.log
&& chown www-data:www-data /var/log/cron.log
&& ln -sf /dev/stdout /var/log/cron.log
COPY start-cron /usr/sbin
COPY cron.d /etc
CMD start-cron
Автор: REDMADROBOT
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/bash/155170
Ссылки в тексте:
[1] Image: https://habrahabr.ru/company/redmadrobot/blog/305364/
[2] подключаемые модули аутентификации (PAM): https://ru.wikipedia.org/wiki/Pluggable_Authentication_Modules
[3] различными хаками: http://stackoverflow.com/questions/2388087/how-to-get-cron-to-call-in-the-correct-paths
[4] тут: https://medium.com/@gchudnov/trapping-signals-in-docker-containers-7a57fdda7d86
[5] Источник: https://habrahabr.ru/post/305364/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.