javascript

Эффективное использование process.env

  • четверг, 28 декабря 2017 г. в 03:13:05
https://habrahabr.ru/company/ruvds/blog/345724/
  • Разработка веб-сайтов
  • Node.JS
  • JavaScript
  • Блог компании RUVDS.com


Если вы только начинаете осваивать Node.js, то, вам, наверняка, встречались примерно такие строчки кода: 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.

Анти-паттерны


Вот несколько распространённых вариантов неправильного использования переменных окружения.

  1. Чрезмерное использование NODE_ENV. Во многих учебных руководствах можно встретить рекомендации по использованию process.env.NODE_ENV, но особых подробностей об этом там можно и не найти. Как результат, наблюдается неоправданное применение NODE_ENV в условных операторах, противоречащее предназначению переменных окружения.
  2. Хранение информации, зависящей от времени. Если приложению требуется SSL-сертификат или периодически изменяющийся пароль для взаимодействия с другим приложением, развёрнутым на том же сервере, будет неразумно задавать эти данные в виде переменных окружения. Сведения об окружении, получаемые приложением, представляют собой состояние среды на момент его запуска и остаются неизменными во время его работы.
  3. Настройка часового пояса. Леон Бамбрик сказал в 2010-м: «В компьютерной науке есть 2 сложные задачи: инвалидация кэша, именование сущностей и ошибки смещения на единицу». Я добавил бы сюда ещё одну: работу с часовыми поясами. При развёртывании приложения в высокодоступных средах его экземпляры могут быть запущены в различных часовых поясах. Один экземпляр может работать в дата-центре, расположенном в Сан-Франциско, другой — в Сингапуре. А пользователи подключаются ко всем этому из Лондона. Рекомендуется, в серверной логике, использовать UTC, а заботы о часовом поясе оставить клиентской части приложения.

Итоги


Правильное использование данных из process.env приводит к разработке приложений, которые легче и удобнее тестировать, развёртывать и масштабировать. Переменные окружения — это одна из тех мелочей, нередко почти незаметных, правильная работа с которыми позволяет сделать код лучше, а неправильная способна привести к неприятностям, которые имеют свойство проявляться в самый неожиданный момент. Надеемся, наш рассказ о переменных окружения поможет вам улучшить качество ваших программ.

Уважаемые читатели! Пользуетесь ли вы process.env в своих проектах на Node.js?