Переменные окружения и PHP

Поговорим про конфигурацию и переменные окружения.

  • Как можно конфигурировать PHP приложение
  • Где хранить секреты и настройки, отличающиеся в разных окружениях (dev vs staging vs prod)
  • Что такое переменные окружения?
  • Проблемы с переменными окружения в PHP проектах
  • Зачем нужны .env файлы?
  • .env файлы в Laravel и Symfony
  • Выводы

Полезные ссылки по теме:

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

Мы можем положить все настройки в отдельный PHP файл в виде массива, некий config.php. Но тут важно отметить два нюанса.

  1. Во-первых, секреты, т.е. логины и пароли в частности к базе данных или к почтовому серверу не должны попадать в git репозиторий – это как минимум не безопасно, и просто не удобно, ведь пароль от базы на production может (и должен) отличаться от пароля от базы на локальной машине – если положить в git, как потом править?
  2. Из этого плавно переходим ко второму нюансу: некоторые настройки зависят от окружения, в котором будет запускаться наше приложение. В production окружении нам нужны одни настройки, а на локальной машине при разработке немного другие. Например, другие логины и пароли к той же самой базе. Обратите внимание на термин «окружение», мы вернёмся к нему чуть позже.

Логичным шагом будет использовать следующий подход: файл config.php не коммитим в репозиторий и добавляем его в .gitignore. Но рядом создаём файл config.example.php в котором можно показать общую структуру конфигурационного массива и даже задать некоторые значения по умолчанию. Этот config.example.php добавляется в git репозиторий, таким образом новый разработчик сделав клон проекта видит пример конфигурации, копирует config.example.php в локальный файл config.php и настраивает под свою машину.

При публикации на production также не забудем создать config.php, наполнив его параметрами подключения к базе и другими секретами. Лучше всего это делать автоматизированно с помощью каких-нибудь инструментов деплоя, но это отдельная тема для разговора.

Кстати, некоторые фреймворки следуя этой же методологии с example конфигом и настоящим конфигом идут ещё дальше в плане удобства, например, фреймворк для тестирования Codeception в комплекте поставки даёт нам файл codeception.dist.yml, который добавляется в git, и отдельно можно создать codeception.yml (без слова dist), который не добавляется в git. Что удобно – сам Codeception автоматически загружает оба файла, при этом значения из codeception.dist.yml имеют меньший приоритет.

Фреймворки общего назначения дают нам достаточно большую гибкость по настройке работы самого фреймворка. Если заглянуть в папку config в Laravel, то увидим там множество различных php файлов описывающих параметры подключения к базе, кеширование, логирование, аутентификацию и многое другое. При этом не все параметры на самом деле являются секретами или зависят от окружения. Есть такие параметры, которые мы задаём на старте разработки проекта и они справедливы для всех окружений и не представляют секрета, например, пути к папкам для шаблонов (config/view.php в Laravel) или имена файлов для логов.

Получается, часть конфигурации является секретной или зависит от окружения и её мы не хотим добавлять в git репозиторий (максимум, мы можем добавить некий example файл в git репозиторий). А часть конфигурации – это по сути зафиксированные для данного проекта значения и их, конечно, нужно сохранять в git.

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

Для секретной или платформозависимой части конфигурации можно использовать так называемые переменные окружения, которые уже давно были изобретены в unix системах. А в наше время к ним подталкивает и методология 12-факторных приложений, и такие инструменты как Docker, Kubernetes и различные сорта Serverless.

Однако в PHP с переменными окружения есть некоторая путаница давайте разберёмся.

Во-первых, в PHP есть суперглобальный массив $_ENVи суперглобальный массив $_SERVER – в оба эти массива попадают переменные окружения. Однако, суперглобальные массивы могут и не существовать – это настраивается в php.ini с помощью параметра variables_order.

Значение по-умолчанию для variables_order таково, что заполняются все суперглобальные массивы. Однако production и development ini файлы, которые идут в поставке с PHP, переопределяют variables_order таким образом, что суперглобальный массив  $_ENV не создаётся. Это сделано, чтобы не тратить время на создание массива $_ENV и рекомендуется использовать функцию getenv().

Переходим к встроенной функции genenv() – да, она позволяет прочитать значение переменных окружения. Однако, функция getenv() не потокобезопасна – если в одном потоке делать getenv, а в другом putenv(), то можно вызвать падение с segmentation fault. Впрочем, как часто мы пишем PHP приложения с тредами? Иными словами, проблема достаточно узкая.

Итак, у нас есть переменные окружения, которые предоставляет нам операционная система (и Linux, и Windows, и macOS). Есть средства чтобы их прочитать из PHP приложения. Казалось бы найдено идеальное место для хранения секретов и настроек зависящих от окружения! Но как эти переменные окружения задать? Тут целая наука.

В Linux есть файл /etc/environment, есть /etc/profile, есть деректория /etc/profile.d, далее переменные окружения можно установить при настройке systemd для конкретно сервиса (в нашем случае для php-fpm), можно указать в конфиге php-fpm, можно пробросить переменные окружения из настроек nginx. Каждый способ имеет право на жизнь в той или иной ситуации, но не рекомендую использовать их все сразу, нужно ведь ещё не запутаться в приоритете.

Ещё проблема: если мы храним секреты в переменных окружения и они, соответсвенно, доступны в суперглобальном массиве $_SERVER, то эти секреты могут утечь! Например, все значения из $_SERVER выводятся на экран при вызове функции phpinfo(). Признайтесь, кто не создавал файл phpinfo.php в публичной директории проекта на production, чтобы понять что там вообще установлено? Все создавали.

А если всё содержимое $_SERVER будет показано на экран какой-нибудь красивой отладочной страницы при возникновении не пойманного исключения? Конечно, в production никаких красивых отладочных страниц быть не должно, но потенциально неприятный момент, о котором нужно помнить. Также содержимое $_SERVER отправляется на сервисы отслеживания ошибок, такие как Sentry или Rollbar. Да, можно настроить санитайзинг отправляемых данных, но об этом надо позаботиться самому.

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

Ещё один нюанс: php-fpm по умолчанию не передаёт переменные окружения, заданные операционной системой, в свои процессы-воркеры. За это отвечает настройка clear_env в файле конфигурации пула php-fpm (обычно у нас один пул, который называется www и его конфигурация соответственно в файле www.conf). С этим сталкиваешься, когда пытаешься пробросить переменные окружения в php-fpm внутри Docker контейнера.

Слишком много мороки с настройкой этих переменных окружения!

Однако, есть ещё так называемые .env файлы. Что это такое? Говорят, их придумали в Ruby on Rail. Это простой текстовый файл в котором мы можем описать переменные окружения и затем наше приложение при запуске прочитает этот файл и распарсит его, наполнив переменные окружения текущего процесса. Для разработчика это достаточно удобный вариант. Кроме того, если я разрабатываю несколько проектов на своей локальной машине и мне реально нужны разные значения переменных окружения под каждый проект, при этом названия переменных окружения совпадают – что делать? Как это разрулить на уровне операционной системы? С помощью Docker – элементарно. Но если я не использую Docker или дело было 5 лет назад, когда ещё никто не использовал Docker? Короче, иметь описание переменных окружения под рукой в папочке проекта в некоем текстовом .env файле – это удобно.

Но давайте посмотрим на это шире. По сути, мы вернулись к той же самой истории с конфигурационным файлом, как его не назови: .env или config.php. Мы его не коммитим в git, так как в нём секреты и настройки зависящие от окружения. А рядом появляется .env.example для удобства документирования. Те же яйца, только в профиль. Разница лишь в том, что мы описываем конфигурацию не в формате php массива, а в формате переменных окружения в .env файле.

.env файлы по задумке не рекомендуется использовать в production. Это удобное текстовое описание для конфигурации в процессе разработки, но в production лучше всё-таки пользоваться переменными окружения, предоставляемыми операционной системой.

Поскольку PHP запускается и умирает на каждый запрос – каждый раз парсить .env файл можно быть накладно. Для решения проблемы с производительностью в Laravel есть команда artisan config:cache, которая парсит .env файл, а также склеивает все многочисленные .php конфиги из папки config в один большой php файл конфигурации. 

Именно поэтому в коде своего Laravel приложения нельзя использовать функцию-хелпер env() и стандартную getenv() – они ничего не вернут, если конфиг уже закэширован с помощью artisan config:cache. В коде приложения (во всех местах, за исключением самих конфигов в папке config) для чтения параметров конфигурации нужно использовать специальную функцию config().

Получается, в Laravel приложении на production на самом деле переменные окружения никак не используются! Они лишь на секунду создаются при чтении .env файла в момент вызова artisan config:cache, что мы делаешь один раз при деплое.

Теперь поговорим про Symfony, который в ноябре 2018 года немного поменял свой подход к .env файлам, что ещё больше запутывает.

Итак, мы договорились, что .env файл – это файл в котором хранятся настройки зависящие от окружения и секреты, его мы не добавляем в git. А рядом у нас есть .env.example, который добавляем в git.

В какой-то момент разработчики фреймворка Symfony подумали и сказали: «у нас теперь всё будет наоборот!» Файл .env – это теперь файл с настройками по умолчанию или примером конфигурации, не будем класть в него секреты или специфичные от окружения настройки, зато его можно (и нужно) добавлять в git. По большому счёту они переименовали .env.example в просто .env.

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

Кроме этого, вводятся файлы .env.dev, .env.staging, .env.prod или любое другое название окружения .env.<environment> и эти файлы, внимание, нужно добавлять в git. Это по задумке дефолтная конфигурация подогнанная под конкретное окружение. Естественно, эти файлы не должны содержать секреты. А поверх них мы можем создать файлы с секретами с именами .env.dev.local.env.staging.local и .env.prod.local – файлы оканчивающиеся на local не добавляем в git. При этом все .env файлы загружаются автоматически и у них есть определённый приоритет! Звучит достаточно запутанно, но логика есть, пользоваться этим безусловно можно, если разораться как.

Подводя итог, сформулируем несколько тезисов:

  1. Самый простой дедовский способ – это конфигурация в файле config.php, который не нужно коммитить в git. Для наглядности в git можно положить config.example.php.
  2. В операционных системах есть идиоматичный способ передачи конфигурационных параметров приложениям – переменные окружения, которые стали ещё более актуальными с приходом Docker.
  3. Использование переменных окружения в PHP сопряжено с дополнительными телодвижениями: не забыть в конфиге php-fpm выключить clear_env, либо пробрасывать их через fastcgi параметры из конфига nginx.
  4. Также в PHP имеем три способа доступа:  через суперглобальные массивы $_SERVER и $_ENV, и через функцию getenv(), а ещё есть putenv() и возможность писать в эти суперглобальные массивы – попробуй угадай что на что повлияет
  5. Поскольку задание настоящих переменных окружения на уровне процессов операционной системы не всегда удобно, были придуманы .env файлы – некая эмуляция переменных окружения.
  6. В разных фреймворках подход к .env файлам разный:
    • в Laravel принято хранить секреты в .env, который не добавляется в git, а рядом держать .env.example отслеживаемый в git;
    • В Symfony наоборот, обычный .env используется для значений по умолчанию и он добавляется в git, а секреты принято хранить в .env.local, который не добавляется в git.
      Не перепутай!
  7. В итоге в production Laravel приложении конфиг кэшируется в один большой php файл в момент деплоя и никаких переменных окружения по факту мы не используем
  8. Внимание вопрос: если в production Laravel приложении конфиг кэшируются в момент деплоя, как быть с запуском Laravel приложения в Docker? Ведь мы хотим следовать методологии один образ и для тестов и для staging и для production.
  9. Забыл упомянуть, что переменные окружения – это строки. Если нужны числа или булевы значения или какие-то вложенные структуры, нужно опять же парсить, придумывать свои правила конвертации. Благо есть целый набор PHP библиотек, в которых эти вопросы уже продуманы.

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

И до следующего выпуска.

Источник: 5minphp.ru

 

Добавить комментарий

%d такие блоггеры, как: