Если вы только начинаете осваивать Node.js, то, вам, наверняка, встречались примерно такие строчки кода:
Глобальная переменная
До тех пор пока приложение не развёрнуто, будь то код, реализующий простейший сайт, или сложное API, используемое в тяжёлых вычислениях, оно совершенно бесполезно. Это — лишь строки текста, хранящиеся в файлах.
Занимаясь созданием программ, разработчик никогда точно не знает, где именно они будут работать. Например, если в процессе разработки нужна база данных, мы запускаем её экземпляр локально и связываемся с ней с использованием строки соединения, которая выглядит примерно так:
Если предположить, что переменными окружения мы не пользуемся, есть два варианта действий.
Первый заключается в том, чтобы обеспечить доступность базы данных на той же машине, на которой работает приложение, что позволит подключиться к ней по адресу
Второй вариант предусматривает модификацию кода таким образом, чтобы в ходе его выполнения, через условный оператор с несколькими ветвями
Подобные конструкции увеличивают число путей выполнения программы, что усложняет тестирование, да и о красоте кода тут говорить не приходится.
Если же для указания строки подключения к базе используются переменные окружения, нашу задачу можно решить так:
При таком подходе можно как подключаться к локальному экземпляру СУБД при разработке, так и организовать соединение с чем-то вроде защищённого удалённого кластера баз данных, поддерживающего балансировку нагрузки и умеющего масштабироваться независимо от приложения. Это даёт возможность, например, иметь множество экземпляров приложения, которые независимы от конкретного экземпляра СУБД.
В целом, рекомендовано всегда рассматривать зависимости приложения от неких внешних служб как присоединённые ресурсы и настраивать подключение к ним с помощью переменных окружения.
Действия, которые выполняются для того, чтобы подготовить переменные окружения для приложения называют предоставлением ресурсов (provisioning) или подготовкой к работе. При подготовке сервера можно выделить два уровня, на которых можно работать с переменными окружения: уровень инфраструктуры и уровень приложения. Подготовить окружение можно, либо используя специализированные инструменты, либо — некую логику, реализованную на уровне приложения.
Среди средств, работающих на уровне приложения, можно отметить пакет
Загрузка переменных окружения выполняется с помощью следующей простой команды:
Такой подход удобен в процессе разработки, но не рекомендуется в продакшне, поэтому, в частности, файл
На инфраструктурном уровне для настройки окружения можно использовать средства для управления развёртыванием приложений вроде PM2, Docker Compose и Kubernetes.
PM2 использует файл
Docker Compose, аналогичным образом, позволяет задавать свойство
У Kubernetes есть похожее свойство
Настройки приложения не влияют на то, какие именно действия выполняет это приложение, не влияют на его логику. Например, приложению известно, что ему необходимо прослушивать порт для того, чтобы к нему можно было обратиться извне, но ему необязательно знать — какой именно это будет порт. Такие данные вполне подходят для вынесения их в переменные окружения. Поэтому их установку и подготовку сетевой инфраструктуры можно доверить средствам развёртывания приложений.
Переменные окружения часто используют для указания того, как приложение должно подключаться к службам, от которых оно зависит. Это позволяет сделать код чище и улучшить тестируемость приложения. В частности, такой подход позволяет окружению тестирования передавать приложению некие условные данные, которые, например, имитируют внештатные ситуации, что позволяет проверить приложение на предмет сбоев в подобных ситуациях. Тут мы имеем дело с похожей ситуацией: приложение нуждается в некоей службе, но где именно она расположена, заранее неизвестно. Настройку переменных окружения для подобных случаев можно доверить менеджерам развёртывания.
В ходе локальной разработки обычно полезно иметь некие программные средства, которые позволяют быстро получать информацию из выполняющегося приложения или изолировать ошибки. Пример подобных средств — интерактивная перезагрузка веб-страницы после внесения изменений в относящийся к ней код приложения. Подобное поведение можно реализовать с помощью условных конструкций, решения в которых принимаются либо на основании стандартных переменных окружения, вроде
Вот несколько распространённых вариантов неправильного использования переменных окружения.
Правильное использование данных из
Уважаемые читатели! Пользуетесь ли вы
app.listen(process.env.PORT)
. Зачем вбивать в редактор кода шестнадцать символов, когда того же эффекта можно добиться, просто указав номер порта, например — 3000? Предлагаем это выяснить.Что такое process.env?
Глобальная переменная
process.env
доступна приложению во время его выполнения благодаря внутренним механизмам Node. Она представляет собой состояние окружения системы в момент запуска приложения. Например, если в системе задана переменная PATH
, обратиться к ней из приложения можно посредством конструкции process.env.PATH
. Её можно использовать, например, если вам нужно узнать место, где можно найти исполняемые файлы, к которым требуется обратиться из кода.О важности окружения, в котором работает приложение
До тех пор пока приложение не развёрнуто, будь то код, реализующий простейший сайт, или сложное API, используемое в тяжёлых вычислениях, оно совершенно бесполезно. Это — лишь строки текста, хранящиеся в файлах.
Занимаясь созданием программ, разработчик никогда точно не знает, где именно они будут работать. Например, если в процессе разработки нужна база данных, мы запускаем её экземпляр локально и связываемся с ней с использованием строки соединения, которая выглядит примерно так:
127.0.0.1:3306
. Однако, когда мы развёртываем рабочий экземпляр приложения, может возникнуть потребность, например, подключиться к СУБД, расположенной на удалённом сервере, скажем, доступной по адресу 54.32.1.0:3306
.Если предположить, что переменными окружения мы не пользуемся, есть два варианта действий.
Первый заключается в том, чтобы обеспечить доступность базы данных на той же машине, на которой работает приложение, что позволит подключиться к ней по адресу
127.0.0.1:3306
. Такой подход означает сильную привязку приложения к инфраструктуре, его потенциально низкую доступность и невозможность его масштабировать, так как развернуть можно лишь один экземпляр приложения, зависимый от единственного экземпляра СУБД.Второй вариант предусматривает модификацию кода таким образом, чтобы в ходе его выполнения, через условный оператор с несколькими ветвями
else
, можно было выбрать подходящую строку подключения к базе данных:let connectionString;
if (runningLocally()) {
connectionString = 'dev_user:dev_password@127.0.0.1:3306/schema';
} else if (...) {
...
} else if (inProduction()) {
connectionString = 'prd_user:prd_password@54.32.1.0:3306/schema';
}
const connection = new Connection(connectionString);
Подобные конструкции увеличивают число путей выполнения программы, что усложняет тестирование, да и о красоте кода тут говорить не приходится.
Если же для указания строки подключения к базе используются переменные окружения, нашу задачу можно решить так:
const connection = new Connection(process.env.DB_CONNECTION_STRING);
При таком подходе можно как подключаться к локальному экземпляру СУБД при разработке, так и организовать соединение с чем-то вроде защищённого удалённого кластера баз данных, поддерживающего балансировку нагрузки и умеющего масштабироваться независимо от приложения. Это даёт возможность, например, иметь множество экземпляров приложения, которые независимы от конкретного экземпляра СУБД.
В целом, рекомендовано всегда рассматривать зависимости приложения от неких внешних служб как присоединённые ресурсы и настраивать подключение к ним с помощью переменных окружения.
Как использовать переменные окружения
Действия, которые выполняются для того, чтобы подготовить переменные окружения для приложения называют предоставлением ресурсов (provisioning) или подготовкой к работе. При подготовке сервера можно выделить два уровня, на которых можно работать с переменными окружения: уровень инфраструктуры и уровень приложения. Подготовить окружение можно, либо используя специализированные инструменты, либо — некую логику, реализованную на уровне приложения.
Среди средств, работающих на уровне приложения, можно отметить пакет
dotenv
, который позволят загружать переменные окружения из файла .env
. Установить этот инструмент можно так:npm install dotenv --save
Загрузка переменных окружения выполняется с помощью следующей простой команды:
require('dotenv').config();
Такой подход удобен в процессе разработки, но не рекомендуется в продакшне, поэтому, в частности, файл
.env
лучше добавить в .gitignore
.На инфраструктурном уровне для настройки окружения можно использовать средства для управления развёртыванием приложений вроде PM2, Docker Compose и Kubernetes.
PM2 использует файл
ecosystem.yaml
, в котором можно задать переменные окружения с помощью свойства env
:apps:
- script: ./app.js
name: 'my_application'
env:
NODE_ENV: development
env_production:
NODE_ENV: production
...
Docker Compose, аналогичным образом, позволяет задавать свойство
environment
в манифест-файле сервиса:version: "3"
services:
my_application:
image: node:8.9.4-alpine
environment:
NODE_ENV: production
...
...
У Kubernetes есть похожее свойство
env
в шаблоне манифеста, которое также позволяет задавать переменные окружения:kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: my_application
spec:
...
template:
spec:
env:
- name: NODE_ENV
value: production
...
Сценарии использования переменных окружения
▍Настройки приложения
Настройки приложения не влияют на то, какие именно действия выполняет это приложение, не влияют на его логику. Например, приложению известно, что ему необходимо прослушивать порт для того, чтобы к нему можно было обратиться извне, но ему необязательно знать — какой именно это будет порт. Такие данные вполне подходят для вынесения их в переменные окружения. Поэтому их установку и подготовку сетевой инфраструктуры можно доверить средствам развёртывания приложений.
▍Взаимодействие с внешними службами
Переменные окружения часто используют для указания того, как приложение должно подключаться к службам, от которых оно зависит. Это позволяет сделать код чище и улучшить тестируемость приложения. В частности, такой подход позволяет окружению тестирования передавать приложению некие условные данные, которые, например, имитируют внештатные ситуации, что позволяет проверить приложение на предмет сбоев в подобных ситуациях. Тут мы имеем дело с похожей ситуацией: приложение нуждается в некоей службе, но где именно она расположена, заранее неизвестно. Настройку переменных окружения для подобных случаев можно доверить менеджерам развёртывания.
▍Вспомогательные средства разработки
В ходе локальной разработки обычно полезно иметь некие программные средства, которые позволяют быстро получать информацию из выполняющегося приложения или изолировать ошибки. Пример подобных средств — интерактивная перезагрузка веб-страницы после внесения изменений в относящийся к ней код приложения. Подобное поведение можно реализовать с помощью условных конструкций, решения в которых принимаются либо на основании стандартных переменных окружения, вроде
process.env.NODE_ENV
, либо на базе специальных переменных, которые создаёт сам разработчик, наподобие process.env.HOT_RELOADING_ENABLED
.Анти-паттерны
Вот несколько распространённых вариантов неправильного использования переменных окружения.
- Чрезмерное использование
NODE_ENV
. Во многих учебных руководствах можно встретить рекомендации по использованиюprocess.env.NODE_ENV
, но особых подробностей об этом там можно и не найти. Как результат, наблюдается неоправданное применениеNODE_ENV
в условных операторах, противоречащее предназначению переменных окружения.
- Хранение информации, зависящей от времени. Если приложению требуется SSL-сертификат или периодически изменяющийся пароль для взаимодействия с другим приложением, развёрнутым на том же сервере, будет неразумно задавать эти данные в виде переменных окружения. Сведения об окружении, получаемые приложением, представляют собой состояние среды на момент его запуска и остаются неизменными во время его работы.
- Настройка часового пояса. Леон Бамбрик сказал в 2010-м: «В компьютерной науке есть 2 сложные задачи: инвалидация кэша, именование сущностей и ошибки смещения на единицу». Я добавил бы сюда ещё одну: работу с часовыми поясами. При развёртывании приложения в высокодоступных средах его экземпляры могут быть запущены в различных часовых поясах. Один экземпляр может работать в дата-центре, расположенном в Сан-Франциско, другой — в Сингапуре. А пользователи подключаются ко всем этому из Лондона. Рекомендуется, в серверной логике, использовать UTC, а заботы о часовом поясе оставить клиентской части приложения.
Итоги
Правильное использование данных из
process.env
приводит к разработке приложений, которые легче и удобнее тестировать, развёртывать и масштабировать. Переменные окружения — это одна из тех мелочей, нередко почти незаметных, правильная работа с которыми позволяет сделать код лучше, а неправильная способна привести к неприятностям, которые имеют свойство проявляться в самый неожиданный момент. Надеемся, наш рассказ о переменных окружения поможет вам улучшить качество ваших программ.Уважаемые читатели! Пользуетесь ли вы
process.env
в своих проектах на Node.js?