Заставляем сервис php-fpm 5.6, запущенный через systemd, читать глобальные переменные окружения
Это короткий how-to для реализации конфигурации php-сервиса, зависимого от окружения, в котором он запущен. Я буду рад, если кто-то подскажет более изящное решение или поправит в мелочах.
Основная идея
Запускать сервис, микросервисы и зависимые приложения в рамках одной экосистемы, конфигурируемой с помощью переменных окружения.Проблема
В этой статье слишком много раз повторяется «переменные окружения».
Из коробки php-fpm игнорирует глобальные переменные окружения (getenv function), в то время как php cli их может получать.
Предыстория
Этот раздел можно пропустить, если вы уже работали с .env
В данный момент я работаю над проектом, написанном на ZF2. Для конфигурации проекта использовались конфиг-файлы для разных окружений. Это порождает большое количество дубликатов конфигурации в репозитории проекта примерно такого вида:
- session.global.php
- session.local.php.dist
- session.unittest.php.dist
- db.global.php
- db.local.php.dist
- db.unittest.php.dist
- ...
Эти дубликаты приходится постоянно синхронизировать друг с другом. Кроме того, они хранят определённую php-логику внутри себя, что порождает дублирование кода.
Я добавил к проекту библиотеку, которая умеет считывать окружение из .env файла и загружать его в $_ENV (упрощённо).
composer require vlucas/phpdotenv
Открыть public/index.php
После require 'init_autoloader.php' добавить:
$dotenv = new Dotenv\Dotenv(__DIR__ . '/../');
// $dotenv->required('SOME_IMPORTANT'); // можно сделать некоторые переменные обязательными
$dotenv->load(); // можно использовать overload(), тогда файл .env станет более важен, чем глобальные переменные окружения (каскадный принцип)
Кроме того (это совершенно необязательно), добавил helper-функцию env() из laravel, которая является обёрткой над php getenv().
if ( ! function_exists('value'))
{
/**
* Return the default value of the given value.
*
* @param mixed $value
* @return mixed
*/
function value($value)
{
return $value instanceof Closure ? $value() : $value;
}
}
if ( ! function_exists('env'))
{
/**
* Gets the value of an environment variable. Supports boolean, empty and null.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
function env($key, $default = null)
{
$value = getenv($key);
if ($value === false) return value($default);
switch (strtolower($value))
{
case 'true':
case '(true)':
return true;
case 'false':
case '(false)':
return false;
case 'empty':
case '(empty)':
return '';
case 'null':
case '(null)':
return;
}
return $value;
}
}
В composer.json добавить в секцию «autoload»:
"autoload" : {
...,
"files": ["library/Common/Config/env.php"]
},
Затем выполнить composer dumpautoload.
Этот шаг позволил выбросить из репозитория все лишние дубликаты конфиг-файлов (local.php, unittest.php, *.php.dist). Вместо этого в корне проекта появился .env.global со списком всех доступных переменных, которые задействованы в конфигах.
1. Т.к. на рабочей машине в переменных окружения может ничего и нет, да и обмениваться проектом неудобно между разными машинами, библиотека phpdotenv перед запуском приложения считывает .env файл и загоняет его переменные в $_ENV[$name] = $value.
2. Конфигурационные файлы вызывают метод env(), который является обёрткой над php-функцией getenv(), и читает переменные окружения, подставляя значение по-умолчанию по необходимости.
// Примеры использования:
$config['emails']['from'] = env('APP_EMAIL', 'info@myemail.com');
$config['is_production'] = ( 'production' == env('APP_ENV') );
if (env('ZF_DEBUG_TOOLS', false)) {
$config['modules'][] = 'ZendDeveloperTools';
}
3. Файл .env не обязательно заполнять. Можно использовать глобальные переменные окружения или значения по-умолчанию в конфигурации. При отсутствии .env файла бросается exception (особенность библиотеки, не самая правильная), на production сервере её можно вообще не подключать. Для избежания exception, файл необходимо просто создать в корне проекта (touch .env).
4. Файл .env не обязательно должен хранить все доступные переменные проекта. Если в конфигах устанавливаются значения по-умолчанию, достаточно записывать в .env только переменные, отличающиеся в данном окружении.
5. Файл .env не нужно коммитить в репозиторий. Его следует добавить в ignore для системы контроля версий.
6. Чтобы сделать переменную окружения обязательной, в index.php необходимо добавить такую конструкцию:
$dotenv->required('APP_ENV'); // Переменная APP_ENV после этого должна в обязательном порядке быть установленной через .env или через окружение
7. В репозитории проекта можно коммитить файлы вида .env.* (.env.phpunit, .env.develop). Это не что иное, как закладки с набором переменных для разного окружения. Оркестратор или CI-система просто копирует шаблон (или переменные из него) при разворачивании проекта там, где проект разворачивается в нескольких копиях в рамках одной системы или нет возможности оперировать глобальными переменными окружения. Закладки удобно сравнивать друг с другом. Эти закладки никак не участвуют в логике сервиса.
Важно: .env.production не должен храниться в репозитории проекта.
Удобно создать .env.default – файл, который содержит все переменные окружения, поддерживаемые в проекте на текущий момент (максимально-возможный template для .env).
– Так что, теперь все конфиги нужно дублировать в .env? Когда добавлять новую переменную окружения?
Выносить конфигурацию в окружение стоит в том случае, если эта опция может отличаться на разных хостах.
Нет ничего плохого в том, чтобы записать нечто в переменные среды.
$config['csv_separator] = ' | '; // это не меняется в разном окружении, не стоит это трогать.
$config['like_panel'] = true; // а это меняется, можно заменить на env('APP_LIKE_PANEL', false)
$config['facebook_app_id'] = 88888888881; // рекомендуется использовать переменные среды
– А как быть с паролями и чувствительными данными?
Храните production-данные в отдельном репозитории, в хранилище паролей или, например, в защищенном хранилище оркестратора.
Итак, проект теперь учитывает окружение, но...
Пока разработка велась на рабочих машинках, проект читал .env файл и всё работало. Но когда я развернул тестовую среду, оказалось, что если задать взаправдашние системные переменные окружения, php-fpm их игнорирует. Различные рецепты из гугла и StackOverflow сводились к той или иной автоматизации использования двух известных способов:
1. Передача переменных через nginx параметром fastcgi_param SOMEENV test;
2. Установкой переменных в формате env[SOME_VAR] в конфигурации пула рабочих процессов php-fpm.
И первый, и второй вариант, удобны для каких-то особых ситуаций. Но если мыслить в парадигме «конфигурировать среду, а не приложение», то подобные способы оказываются куда труднее, чем например просто положить .env файл в папку с проектом. Но ведь оркестратор, CI-система или просто системный администратор не должен знать детали реализации проекта, это не изящно.
Предлагаемый способ решения
Скомбинировав различные рецепты из сети, я нащупал следующее рабочее решение.
Тестировалось под Centos 7, PHP 5.6.14.
1. Открыть /etc/php.ini
- Заменить
variables_order = "GPCS"
на
variables_order = "EGPCS"
# После этого PHP добавит в глобальное пространство переменные окружения
# http://php.net/manual/ru/ini.core.php#ini.variables-order
2. Открыть /etc/php-fpm.d/www.conf, не путать с /etc/php-fpm.conf (в разных системах может быть в разном месте, это конфиг www-пула процессов для php-fpm.
- Добавить (или заменить, если вдруг есть):
clear_env = no # выключить очистку глобальных переменных для запускаемых воркеров
3. Установить необходимые переменные окружения в /etc/environment (стандартный синтаксис A=B)
4. ln -fs /etc/environment /etc/sysconfig/php-fpm # теперь конфиг переменных окружения сервиса php-fpm будет просто ссылкой на глобальный конфиг
5. systemctl daemon-reload && service php-fpm restart
Этот же подход с симлинком, в теории, применим и к другим сервисам.
Плюсы предложенного решения:
— Переменные, хранящиеся в /etc/environment, доступны разным приложениям. Можно вызвать echo $MYSQL_HOST в shell или getenv('MYSQL_HOST') в php.
— Переменные окружения, которые явно не заданы в /etc/environment, не попадут в php-fpm. Это позволяет с помощью оркестратора контролировать окружение извне изолированной системы, в которой запущен сервис.
Минусы:
— К сожалению, у php-fpm я не нашел работающей команды для reload по аналогии с nginx, так что в случае изменения /etc/environment, обязательно нужно делать systemctl daemon-reload && service php-fpm restart.
Важно: если ваше приложение работает не в изолированной среде (сервер, виртуалка, контейнер), определение переменных окружения может непредсказуемо повлиять на соседние сервисы в системе из-за совпадений имён в глобальном пространстве.
Ссылки:
— Для тех, кто не читал статью
— Методология двенадцати факторов разработки SAAS: храните конфигурацию в окружении (англ.)
— Загрузка переменных окружения с помощью .env-файлов для development environment в php-проектах.