Вы неправильно используете переменные окружения — взгляд с точки зрения Node.js


TL;DR

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

О, что?

Переменные окружения — это просто, скажете вы, мы работаем с переменными окружения всю свою карьеру… как мы можем «делать их неправильно»?! Ну, как сказал американский компьютерный ученый Джим Хорнинг: «Ничто не бывает таким простым, как мы надеемся». И в данном случае риск возникает каждый раз, когда вы «устанавливаете и забываете» переменную. Давайте рассмотрим проблему, вернее, проблемы.

Начнем с самого начала

Итак, что такое переменные окружения и зачем мы их используем? Проще говоря, переменные окружения — это части состояния (читай, строковые значения), которые мы храним в «окружении», в котором работает наше приложение. Это состояние обычно задается с помощью одного из механизмов, предоставляемых операционной системой, оболочкой или контейнерным оркестратором, который отвечает за процесс нашего приложения.

Переменные окружения — это простой механизм, и это хорошо, потому что многие инженерные решения не так просты.

«Простота — необходимое условие надежности. » — Эдсгер Дейкстра.

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

Видите, это в основном плюсы!

Да, плюсов очень много. Как мы увидим, хранение состояния в среде позволяет нам делать несколько очень полезных вещей, которые в противном случае были бы рискованными или трудоемкими.

1. Менять конфигурацию по своему усмотрению

Мы можем изменять поведение нашего приложения, избегая рискованных действий, таких как изменение исходного кода, и трудоемких операций, таких как перекомпиляция, повторное развертывание, тестирование и так далее. Если нам нужно изменить API-ключи, включить или выключить флаги функций или настроить другое поведение, мы можем сделать все это, не вставая с кресла, просто развернув новые значения и перезапустив наше приложение.

2. Хранить секреты в тайне

Мы можем хранить секреты отдельно от исходного кода. Это поможет нам снизить риск, связанный с чувствительными значениями, такими как ключи API, учетные данные и т.д., которые могут подвергнуть риску наших пользователей, если они будут раскрыты. Таким образом, если злоумышленники получат доступ к нашему исходному коду, они не смогут одновременно завладеть и секретами. Таким образом, им будет сложнее нанести нам ущерб.

3. Оставайтесь на правильной стороне регулирования

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

4. Установите разные значения для каждого инженера или среды

При локальной работе нам часто приходится использовать различные значения для ключей API, флагов функций и флагов поведения, которые имеют смысл при разработке, но не в развернутой среде. То же самое можно сказать и об автоматизированном тестировании, когда тестам может потребоваться изменить поведение приложения и входные данные для проверки определенных аспектов.

Каждой развернутой среде может быть присвоен свой набор переменных окружения, например, чтобы секреты производства были изолированы и отделены от секретов постановки. Как и в случае локальной разработки, мы также можем изменять значения в наших средах постановки/тестирования независимо от других сред по мере необходимости. Гибкость — это здорово!

5. Используйте файлы dot env

В обширной вселенной JavaScript распространенным шаблоном является использование пакета dot-env для чтения переменных окружения из локального файла .env, который не зафиксирован в репозитории. Это гораздо более быстрая (и, что важно, более наглядная) альтернатива установке переменных окружения в реальном окружении. Инженеры могут быстро и легко изменять значения в процессе разработки по мере возникновения необходимости.

Так в чем же проблема?

Их несколько. Это все риски, которые мы должны смягчить, уязвимости, которые могут оставить нас открытыми для атак, и ошибки, которые могут вызвать неожиданное поведение в самый неподходящий момент. Даже в самом лучшем случае плохое поведение переменных окружения может привести к потере значительного количества времени, особенно в динамически типизированных языках, таких как JavaScript.

«Ищите простоту, но не доверяйте ей». — Альфред Норт Уайтхед.

Нам нужно быть осторожными, чтобы не попасть в одну из бесчисленных ловушек. В каждом случае трудно, если вообще возможно, предсказать, как поведет себя наше приложение. Иногда проблемы очевидны сразу, но во многих случаях мы не узнаем о проблеме до тех пор, пока она случайно не всплывет в самый неподходящий момент.

1. Пропущенные значения

Наиболее очевидный риск здесь заключается в том, что значение может отсутствовать. Это более вероятно на наших локальных машинах, когда один из разработчиков вносит изменение, требующее переменной окружения, которую мы не установили в нашем локальном окружении. Это менее вероятно в развернутом коде, который прошел через несколько уровней проверки и тестирования, но все же это может произойти в сложных системах. В конце концов, мы всего лишь люди!

LOG_LEVEL="TRACE"
#API_KEY="..."
DATABASE_URL="..."
Вход в полноэкранный режим Выход из полноэкранного режима

Упс, мы отключили значение API_KEY и забыли об этом. Или, возможно, наш коллега добавил ACCESS_TOKEN_TTL в своем последнем коммите, а вы не заметили, что нужно добавить его в ваш локальный файл .env.

2. Пустые значения

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

LOG_LEVEL=""
Вход в полноэкранный режим Выйти из полноэкранного режима

Что именно означает вышесказанное? Значит ли это, что мы хотим полностью отключить ведение журнала? Значит ли это, что мы хотим использовать уровень журнала по умолчанию, и нам все равно, какой он? Или (что более вероятно) что-то сломалось, и нам нужно это исправить? Спросите своих друзей, и вы можете обнаружить, что их ожидания расходятся с вашими.

3. Произвольные значения

Переменные окружения часто используются для булевых значений, таких как флаги функций. Булевы значения имеют некоторые большие недостатки, о которых я не буду здесь рассказывать, но можно сказать, что эти булевы значения произвольны, и разные инженеры будут использовать разные значения.

Например:

FEATURE_FLAG_AAA="true"
FEATURE_FLAG_B="TRUE"
FEATURE_FLAG_c="yes"
FEATURE_FLAG_c="Y"
FEATURE_FLAG_c="1"
Войти в полноэкранный режим Выйти из полноэкранного режима

Как люди, мы сразу же понимаем, что все эти значения означают одно и то же, что флаг определенной функции был включен. Мы полагаемся на условности и последовательность, чтобы не попасть в ловушку использования разных значений в разных местах, но благие намерения не всегда помогают, когда пасут кошек 🐈 (инженеры).

То же самое можно сказать, если вы используете значения перечислений, например, в уровнях журналов (INFO, DEBUG, TRACE и т.д.). Очевидно, что в итоге вы можете получить недопустимое значение, которое может поставить крест на работе, если вы не подтвердите значение, считанное из переменной… но многие ли из нас делают это? 🌚

4. Неправильные типы

Выше мы рассмотрели проблему с булевыми значениями, аналогичная история возникает, если вам нужно использовать значение в виде числа. Переменные окружения всегда считываются как строки, независимо от того, какое значение вы в них записали:

FEATURE_FLAG_AAA="true"
SOME_NUMBER="3"
Войти в полноэкранный режим Выйти из полноэкранного режима

Возможно, вам нужно, чтобы значение SOME_NUMBER было числом, чтобы TypeScript позволил вам передать его в красивую библиотеку, которую вы хотите использовать. Разбираете ли вы значение до целого числа, как это сделано здесь?

const value = Number.parseInt(process.env.SOME_NUMBER);
someNiceLibrary(value);
Вход в полноэкранный режим Выйти из полноэкранного режима

А что если это значение будет изменено на float в одной среде, но не в другой?

SOME_NUMBER="3.14"
Войти в полноэкранный режим Выход из полноэкранного режима

Внезапно ваше приложение начинает сбиваться, но вы не знаете почему. Вы видите какое-то странное поведение, но не знаете, почему, или, что еще хуже, вы видите стек-трассировку сообщения об ошибке, которая является красной селедкой и направляет вас совершенно не в ту сторону в течение часа, пока ваш клиент кричит на вас.

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

5. Необязательные значения

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

#FEATURE_FLAG_AAA="true" # 1. comment out a value we don't need at the moment.
FEATURE_FLAG_AAA="" # 2. or set it to an empty value (not so good!)
Войти в полноэкранный режим Выйти из полноэкранного режима

Если мы вручную проверяем переменные окружения, чтобы убедиться в их существовании, нам нужно оставить эту переменную не отмеченной, поскольку она может быть необязательной. Это вносит человеческий фактор, благодаря которому будущие инженеры могут не добавлять проверки присутствия там, где это необходимо, потому что они видят, что они не последовательно применяются ко всем переменным. Переменная неявно является необязательной, и это оставляет ее открытой для интерпретации читателем. Лучше явно указывать, когда переменные являются необязательными, так как большинство (т.е. по умолчанию) будут обязательными.

6. Скрытые переменные окружения

Это плохая (но, к сожалению, распространенная) практика для инженеров — читать переменную окружения в тот момент, когда они хотят ее использовать, например:

function calculateCommission(amount: number): number {
  return amount * Number.parseInt(process.env.COMMISSION_RATE);
}
Войти в полноэкранный режим Выйти из полноэкранного режима

В чем здесь проблема? Наша хорошая функция calculateCommission может вести себя странно, если переменная окружения COMMISSION_RATE отсутствует или установлена в какое-то странное значение. Возможно, инженер, написавший это, забыл обновить документацию, чтобы указать, что ставка комиссии должна быть настроена в окружении, а вы не знали, что это нужно сделать. Упс.

7. Поведение и безопасность

Переменные среды — это побочные эффекты. Можно сказать, что они добавляют примеси в наш код. Наше приложение не может контролировать значения, которые оно считывает из среды, и должно принимать то, что ему дают. Это означает, что переменные среды сродни пользовательскому вводу и несут те же риски. ☠️

Значение переменной окружения может быть неожиданным или, что еще хуже, вредоносным. В лучшем случае значение вызовет видимую ошибку, которая приведет вас по садовой дорожке в течение часа или двух, прежде чем вы поймете, что на самом деле вызвало проблему. В худшем случае, вы подвергли свое приложение воздействию входных данных, которым вы не можете доверять (а вы доверяли им абсолютно), не проверив их подлинность или корректность, и теперь вы храните конфиденциальные данные в очереди сообщений злоумышленника в течение последних 2 недель, а не в своей собственной. 😬

Итак, как же нам обойти эти проблемы?

Простота фантастически великолепна, за исключением тех случаев, когда это не так.

«Простое может быть труднее сложного: вам придется потрудиться, чтобы очистить свое мышление, чтобы сделать его простым. Но в конечном итоге это того стоит, потому что, достигнув этого, вы сможете сдвинуть горы». — Стив Джобс.

Хитрость, как и в случае с любым «пользовательским» вводом, находящимся вне сферы нашего контроля, заключается в том, чтобы доверять, но проверять, или, в нашем случае, доверять, но проверять. Есть несколько вещей, которые необходимо сделать для каждого значения, которое вы считываете из окружения:

  1. Проверка наличия — убедитесь, что ожидаемые переменные окружения определены.
  2. Проверка на пустоту — убедитесь, что ожидаемые значения не являются пустыми строками.
  3. Проверка значений — убедитесь, что только ожидаемые значения могут быть установлены.
  4. Приведение к типу — убедитесь, что значения приводятся к ожидаемому типу в момент их считывания.
  5. Единая точка входа — убедитесь, что все переменные втягиваются в одно и то же место, а не разбросаны по всей кодовой базе, чтобы люди могли наткнуться на них позже.
  6. Dot env — считывание значений как из файла .env, так и из окружения.

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

Пакет: safe-env-var

safe-env-vars будет читать переменные окружения из окружения, а также из файла .env безопасным способом с полной поддержкой TypeScript. По умолчанию он будет выдавать ошибку, если переменная окружения, которую вы пытаетесь прочитать, не определена или пуста.

Очень быстро можно приступить к базовому использованию, если все, что вы делаете, это считывание строковых значений, которые всегда необходимы:

import EnvironmentReader from 'safe-env-vars';

const env = new EnvironmentReader();

export const MY_VALUE = env.get(`MY_VALUE`); // string
Войти в полноэкранный режим Выйти из полноэкранного режима

Вы можете явно пометить переменные как необязательные:

export const MY_VALUE = env.optional.get(`MY_VALUE`); // string | undefined
Войти в полноэкранный режим Выйти из полноэкранного режима

Или вы можете позволить переменным быть пустым значением, хотя я бы не рекомендовал этого делать по причинам, указанным в обсуждении выше:

export const MY_VALUE = env.get(`MY_VALUE`, { allowEmpty: true }); // string
Войти в полноэкранный режим Выход из полноэкранного режима

Вы можете даже приводить тип значения, как вы и ожидали:

// Required
export const MY_BOOLEAN = env.boolean.get(`MY_BOOLEAN`); // boolean
export const MY_NUMBER = env.number.get(`MY_NUMBER`); // number

// Optional
export const MY_BOOLEAN = env.optional.boolean.get(`MY_BOOLEAN`); // boolean | undefined
export const MY_NUMBER = env.optional.number.get(`MY_NUMBER`); // number | undefined
Enter fullscreen mode Выйти из полноэкранного режима

И, наконец, вы можете проверить, является ли переменная одним из допустимых значений. Эта проверка всегда происходит после проверки наличия/пустоты и приведения типа значения.

export const MY_NUMBER = env.number.get(`MY_NUMBER`, { allowedValues: [1200, 1202, 1378] ); // number
Вход в полноэкранный режим Выход из полноэкранного режима

Дополнительную информацию об использовании и примеры смотрите в документации.

Рекомендуемый шаблон

Я бы рекомендовал иметь единую точку входа для переменных окружения в вашем приложении. Одно место, где вы считываете все значения, необходимые различным модулям и функциям. Это гарантирует, что есть только одно место для поиска и одно место для изменения при внесении изменений.

Мне нравится структурировать свою единую точку входа в проектах JavaScript/TypeScript следующим образом:

/src/
    /main.ts
    /config/
        /env.ts
        /constants.ts
        /index.ts
Вход в полноэкранный режим Выход из полноэкранного режима

./config/env.ts

import EnvironmentReader from 'safe-env-vars';

const env = new EnvironmentReader();

export const COMMISSION_RATE = env.number.get(`COMMISSION_RATE`); // number
Войти в полноэкранный режим Выйти из полноэкранного режима

./config/constants.ts

export const SOME_CONSTANT_VALUE = 123;
export const ANOTHER_CONSTANT_VALUE = `Hello, World`;
Войти в полноэкранный режим Выход из полноэкранного режима

./config/index.ts

export * as env from './env';
export * as constants from './constants';
Войти в полноэкранный режим Выход из полноэкранного режима

…а использование?

import * as config from './config';

const { COMMISSION_RATE } = config.env;
const { SOME_CONSTANT_VALUE } = config.constants;

export function calculateCommission(amount: number): number {
  return amount * COMMISSION_RATE;
}
Войти в полноэкранный режим Выход из полноэкранного режима

Это дает очень чистый способ работы с настраиваемыми переменными окружения, а также с постоянными значениями. Преимущества такого подхода в том, что существует единственная точка входа для переменных окружения в вашем приложении, и каждое использование этих значений направляет читателя обратно к этой точке входа.

Заключение

Не попадайте в ловушку, полагая, что раз вы используете переменные окружения уже много лет, то они безопасны и не могут вас удивить. Лучше доверять, но проверять значения, которые вы читаете, используя надежную и экономящую время библиотеку, такую как safe-env-vars*, которая сделает всю тяжелую работу за вас.

*Возможны альтернативные варианты. 🙃

Оцените статью
Procodings.ru
Добавить комментарий