Если вы только начинаете осваивать 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?

Комментарии (8)


  1. RidgeA
    27.12.2017 15:23

    Я еще могу добавить что черезмерное обращение к переменным окружения через `process.env` может негативно сказаться на производительности.
    github.com/nodejs/node/issues/3104
    github.com/facebook/react/issues/812

    Если надо часто обращаться к значению в переменных окружения — лучше закешировать.


  1. kolyamb4
    27.12.2017 16:28

    уже задавал вопрос к статье на medium'е, но спрошу ещё тут: чем установка «dotenv» и считывание из файла ".env" лучше, чем нативный module.exports? Например:

    файл token.js (добавляем его в .gitignore)

    module.exports.x = ‘MY TELEGRAM BOT TOKEN HERE’;

    файл index.js
    const TelegramBot = require(“node-telegram-bot-api”),
    token = require(“./token”),
    bot = new TelegramBot(token.x, { polling: true });

    Вроде не надо ставить ничего, а задачи выполняются те же. Или я что-то не понимаю? (я ещё учусь, просьба палками не бить и не минусовать)


    1. RidgeA
      27.12.2017 16:32

      подход с использованием .env используется так же в других языкак и фреймворках + dotenv позволяет переопределить переменны заданные в файле .env переменными окружения. Это же можно сделать, в принципе, и с приведенном в примере

       module.exports.MY_TELEGRAM_TOKEN = process.env.MY_TELEGRAM_TOKEN || ‘MY TELEGRAM BOT TOKEN HERE’;
      


    1. Yngvie
      27.12.2017 17:38

      Вариант с token.js тоже довольно часто используется. Например в Python/Django, часто создают файл settings_local.py. Но такой подход сработает только для интерпретируемых языков, и только если код физически на сервере и его можно править.


      Этот подход не сработает, если ваше приложение написано на C#, и на сервере лежит только результат команды dotnet build, без собственно кода. Или если вы делаете приложение в виде пакета, и на сервере ставите его через npm install my-package. Или если вы используете typescript, и перед запуском приложения оно собирается в большой JS файл. Теоретически в последнем случае все еще можно использовать import TOKENS from "tokens", но придется поднапрячься с настройкой, чтобы собранный файл импортировал файл с токенами.


      И второй момент — переменные окружения работают на уровне ОС, чтобы использовать их не надо ничего знать о структуре вашего приложения. Например такой подход применяется в популярном сервисе Heroku. При деплое на Heroku приложение может узнать, какой из портов открыт считав значение process.env.PORT.
      На примерах с Docker в статье — можно собрать один image, и запускать его с разными настройками, не меняя файлы.


    1. MikailBag
      27.12.2017 21:24
      +1

      Это усложняет администрирование.
      Вместо того, чтобы просто передать токен через переменную окружения, придется генерировать js-файл.


  1. baldr
    27.12.2017 20:05
    +1

    Вот интересно, сколько раз нужно человеку показать результат выполнения команды (вместо 1670 — PID любого процесса):

    cat /proc/1670/environ
    чтобы он перестал передавать, например, пароли или токены в переменных окружения.
    В принципе, не для всех процессов это доступно, но, скорее всего, вы создаете нового юзера, даете ему некоторые права для запуска своего приложения, но не задумываетесь о доступе к переменным.
    http://man7.org/linux/man-pages/man5/proc.5.html — для некоторого rtfm.

    Для тех, кто начнет требовать пояснений — реальный пример из жизни… Есть django-based приложение, работает на сервере, предоставляет некий API. Рядом крутится фронтенд на WordPress. В один далеко не прекрасный день через одну из кучи дыр в WP сайт дефейсится и на сервере появляются какие-то странные файлы. Скрипт на php залезает в /proc и читает оттуда все что возможно, ищет в environ слова по паттерну «password» и потом их (включите фантазию).
    Если django-приложение не использует переменные окружения, а хранит свои пароли, скажем, в файле /usr/local/etc/secrets.yaml с ro-правами для конкретного юзера — то php-скрипт, запущенный из-под www-data (или кто там еще) не сможет ничего увести (по крайней мере этим способом).


    1. ALexhha
      28.12.2017 15:27

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

      P.S.
      в вашем случае вы упускаете один нюанс, что если зловред будет на самой джанге, то ваш способ ничего не даст


      1. baldr
        28.12.2017 15:55

        Все так. Решение с WP было не моим, но расхлебывать пришлось мне.
        Для джанги количество общеизвестных багов (которые эксплуатируют сканеры) на несколько порядков меньше чем для LAMP/WP, но да, все так.