TL;DR

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

О, что?

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

Начнем сверху

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

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

«Простота является предпосылкой надежности. “ — Эдсгер Дейкстра.

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

Видите, это в основном вверх!

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

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);

А что, если это значение изменится на число с плавающей запятой в одной среде, но не в другой?

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

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

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*, которая делает всю тяжелую работу за вас.

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