Поговорим про конфигурацию и переменные окружения.
- Как можно конфигурировать PHP приложение
- Где хранить секреты и настройки, отличающиеся в разных окружениях (dev vs staging vs prod)
- Что такое переменные окружения?
- Проблемы с переменными окружения в PHP проектах
- Зачем нужны .env файлы?
- .env файлы в Laravel и Symfony
- Выводы
Полезные ссылки по теме:
- https://mattallan.me/posts/how-php-environment-variables-actually-work/
- https://phpprofi.ru/blogs/post/72
- https://laravel.com/docs/5.8/configuration
- https://symfony.com/doc/current/configuration/dot-env-changes.html
- https://symfony.com/doc/current/configuration.html#config-env-vars
Допустим, у нас есть PHP приложение. Приложению нужна какая-то конфигурация, как минимум настройки подключения к базе данных, возможно настройки подключения к Redis, к почтовому серверу.
Мы можем положить все настройки в отдельный PHP файл в виде массива, некий config.php
. Но тут важно отметить два нюанса.
- Во-первых, секреты, т.е. логины и пароли в частности к базе данных или к почтовому серверу не должны попадать в git репозиторий – это как минимум не безопасно, и просто не удобно, ведь пароль от базы на production может (и должен) отличаться от пароля от базы на локальной машине – если положить в git, как потом править?
- Из этого плавно переходим ко второму нюансу: некоторые настройки зависят от окружения, в котором будет запускаться наше приложение. В 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
файлы загружаются автоматически и у них есть определённый приоритет! Звучит достаточно запутанно, но логика есть, пользоваться этим безусловно можно, если разобраться как.
Подводя итог, сформулируем несколько тезисов:
- Самый простой дедовский способ – это конфигурация в файле
config.php
, который не нужно коммитить в git. Для наглядности в git можно положитьconfig.example.php
. - В операционных системах есть идиоматичный способ передачи конфигурационных параметров приложениям – переменные окружения, которые стали ещё более актуальными с приходом Docker.
- Использование переменных окружения в PHP сопряжено с дополнительными телодвижениями: не забыть в конфиге
php-fpm
выключитьclear_env
, либо пробрасывать их через fastcgi параметры из конфига nginx. - Также в PHP имеем три способа доступа: через суперглобальные массивы
$_SERVER
и$_ENV
, и через функциюgetenv()
, а ещё естьputenv()
и возможность писать в эти суперглобальные массивы – попробуй угадай что на что повлияет - Поскольку задание настоящих переменных окружения на уровне процессов операционной системы не всегда удобно, были придуманы
.env
файлы – некая эмуляция переменных окружения. - В разных фреймворках подход к
.env
файлам разный:- в Laravel принято хранить секреты в
.env
, который не добавляется в git, а рядом держать.env.example
отслеживаемый в git; - В Symfony наоборот, обычный
.env
используется для значений по умолчанию и он добавляется в git, а секреты принято хранить в.env.local
, который не добавляется в git.
Не перепутай!
- в Laravel принято хранить секреты в
- В итоге в production Laravel приложении конфиг кэшируется в один большой php файл в момент деплоя и никаких переменных окружения по факту мы не используем
- Внимание вопрос: если в production Laravel приложении конфиг кэшируются в момент деплоя, как быть с запуском Laravel приложения в Docker? Ведь мы хотим следовать методологии один образ и для тестов и для staging и для production.
- Забыл упомянуть, что переменные окружения – это строки. Если нужны числа или булевы значения или какие-то вложенные структуры, нужно опять же парсить, придумывать свои правила конвертации. Благо есть целый набор PHP библиотек, в которых эти вопросы уже продуманы.
Конфигурирование и переменные окружения — казалось бы, тема простая, но есть своя глубина и разные подходы. Копайте глубже, это интересно!
И до следующего выпуска.