- PVSM.RU - https://www.pvsm.ru -

Запуск cron внутри Docker-контейнера

Запуск cron внутри Docker-контейнера - 1 [1]
Так уж вышло, что запуск cron в Docker-контейнере — дело весьма специфическое, если не сказать сложное. В сети полно решений и идей на эту тему. Вот один из самых популярных (и простых) способов запуска:

cron -f

Но такое решение (и большинство других тоже) обладает рядом недостатков, которые сходу обойти достаточно сложно:

  • неудобство просмотра логов (команда docker logs не работает)
  • cron использует свой собственный Environment (переменные окружения, переданные при запуске контейнера, не видимы для cron заданий)
  • невозможно нормально (gracefully) остановить контейнер командой docker stop (в конце концов в контейнер прилетает SIGKILL)
  • контейнер останавливается с ненулевым кодом ошибки

Logs

Проблему просмотра логов с использованием стандартных средств 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

Environment variables

Изучая информацию на тему назначения переменных окружения для задач 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"]

А его код при этом должен быть написан специальным образом:

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-заданий, то в этом случае можно использовать такой скрипт:

docker-entrypoint.sh

#!/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), мы сможем убедиться в работоспособности этого решения:

print_env

* * * * * www-data env >> /var/log/cron.log 2>&1

Dockerfile

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  # нужно подождать минуту

Graceful shutdown

Говоря о причине невозможности нормального завершения описанного контейнера с 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 в этом случае просто отказывается реагировать на какие-либо сигналы.

True graceful shutdown with zero exit code

Остается только одно — написать отдельный скрипт запуска демона cron, умеющий при этом правильно реагировать на управляющие сигналы. Относительно легко, даже если раньше на bash'е писать не приходилось, можно найти информацию о том, что в нем есть возможность запрограммировать обработку сигналов (при помощи команды trap). Вот как, к примеру, мог бы выглядеть такой скрипт:

start-cron

#!/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, которыми я пользуюсь сейчас.

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 $!

Dockerfile

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