Заставляем сервис 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 (упрощённо).

Подключение библиотеки vlucas/phpdotenv к ZF2

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().

Добавляем метод env($key, $default = null) в ZF2

Создать файл, например library/Common/Config/env.php, с содержимым:

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['cvs_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. service php-fpm restart 

Этот же подход с симлинком, в теории, применим и к другим сервисам.

Плюсы предложенного решения:
— Переменные, хранящиеся в /etc/environment, доступны разным приложениям. Можно вызвать echo $MYSQL_HOST в shell или getenv(‘MYSQL_HOST’) в php.
— Переменные окружения, которые явно не заданы в /etc/environment, не попадут в php-fpm. Это позволяет с помощью оркестратора контролировать окружение извне изолированной системы, в которой запущен сервис.

Минусы:
— К сожалению, у php-fpm я не нашел работающей команды для reload по аналогии с nginx, так что в случае изменения /etc/environment, обязательно нужно делать service php-fpm restart.

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

Ссылки:
— Методология двенадцати факторов разработки SAAS: храните конфигурацию в окружении (англ.)
— Загрузка переменных окружения с помощью .env-файлов для development environment в php-проектах.

ссылка на оригинал статьи http://habrahabr.ru/post/270359/