Переменные окружения и PHP
Допустим, у нас есть 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. и эти файлы, внимание, нужно добавлять в 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.
Не перепутай!
- В итоге в production Laravel приложении конфиг кэшируется в один большой php файл в момент деплоя и никаких переменных окружения по факту мы не используем
- Внимание вопрос: если в production Laravel приложении конфиг кэшируются в момент деплоя, как быть с запуском Laravel приложения в Docker? Ведь мы хотим следовать методологии один образ и для тестов и для staging и для production.
- Забыл упомянуть, что переменные окружения – это строки. Если нужны числа или булевы значения или какие-то вложенные структуры, нужно опять же парсить, придумывать свои правила конвертации. Благо есть целый набор PHP библиотек, в которых эти вопросы уже продуманы.
Конфигурирование и переменные окружения — казалось бы, тема простая, но есть своя глубина и разные подходы. Копайте глубже, это интересно!