В этой статье мы рассмотрим переменные окружения (environment variables). По сути, это пары ключ-значение набора данных, которые хранятся на уровне операционной системы.

В этой статье мы узнаем о переменных окружения в Node.js с примерами. Содержание:

Переменные окружения

Переменные окружения — это набор данных в виде пар ключ-значение, доступных на уровне операционной системы. Этот набор данных доступен во всех основных оболочках командной строки операционных систем Windows, Mac и Linux.

Почему переменные окружения важны?

  • Разделение проблем

  • Безопасность

  • Переносимость

  • Масштабируемость

  • Совместимость

  • Интероперабельность

Необходимые условия для этого проекта

Предполагается, что у вас есть:

  1. Базовые знания Node.js

  2. Базовые знания JavaScript

  3. Некоторые знания о разработке бэкенда

Установка Node и настройка проекта

Установить Node.js на машину можно разными способами. Для начала нужно зайти на официальный сайт Node и скачать оттуда нужную версию.

Давайте рассмотрим установку Node.js в операционной системе Linux, предпочтительно Ubuntu

Шаг 1. Откройте терминал

Шаг 2. Используйте скрипт curl или wget для установки nvm, используя git-репозиторий nvm. Запустите скрипт для установки nvm

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
# OR
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash

Шаг 3. Закройте и снова откройте терминал и выполните следующие действия, чтобы применить скрипт

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

Шаг 4. Подтверждение установки

Убедиться, что NVM установлен в вашей системе, можно с помощью следующей команды:

nvm --version

*Шаг 5. * Установка Node

Введите следующую команду для установки последней версии node:

nvm install latest

или

nvm install --lts

Шаг 6. Установка версии по умолчанию

Чтобы установить версию Node, которая будет использоваться в вашей системе по умолчанию, используйте команду:

nvm alias default 20.8.0

Шаг 7. Проверка установки 

Чтобы проверить, установлен ли Node, введите команду:

node -v # сообщает текущую версию node в вашей системе. В нашем случае это 20.8.0

Инициализация проекта

Теперь, когда мы установили Node, давайте создадим новый проект, в котором мы будем использовать переменную окружения.

Создайте новую директорию, назовите ее env-demo и перейдите в нее:

mkdir env-demo
cd env-demo

Теперь введите команду для инициализации нового проекта и заполните поля по своему усмотрению.

npm init
package name: (env-demo) 
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 

В итоге будет создан файл package.json, который будет выглядеть примерно так:

{
  "name": "env-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Инициализация первой переменной окружения

Шаг 1: Установите пакет dotenv

Сейчас у вас инициализирован проект в приведенном выше разделе и создан файл package.json. Чтобы установить пакет node dotenv, который используется для создания env-файлов, введите в терминале команду:

npm install dotenv --save

Пакет dotenv будет установлен и сохранен как зависимость в файле package.json. Это должно выглядеть примерно так:

{
  "name": "env-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^16.3.1"
  }
}

Шаг 2 Создание файла .env

  • В корневой папке создайте новый файл и назовите его .env.

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

DATABASE_URL=sample-database-url
PORT=3000
SECRET_KEY=mysecretkey

Шаг 3. Загрузка и чтение переменных окружения

В корневой папке создайте файл index.js, затем откройте терминал и введите следующую команду для установки Express.js:

npm install express --save

Express будет установлен и сохранен в качестве зависимости в файле package.json. Файл package.json будет выглядеть следующим образом:

{
  "name": "env-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^16.3.1",
    "express": "^4.18.2"
  }
}

и структура вашего проекта выглядит следующим образом:

Теперь откройте файл index.js и введите следующую команду, чтобы запустить простой веб-сервер.

const express = require('express');
const app = express();
const port = 4000;  // defaults to 4000

app.get('/', (req, res) => {
    res.send('Hello World!')
  })

app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}/`);
});

запустите код с помощью

node index.js

и перейдите на localhost://4000, чтобы получить hello world

Image description

Загрузка и чтение файла .env

Теперь, когда сервер запущен, давайте прочитаем файл .env, который мы создали в предыдущем шаге, и загрузим данные о порте из файла .env.

Откроем файл index.js и «потребуем» там библиотеку dotenv:

require('dotenv').config()

Теперь вы можете получить доступ к файлу .env с помощью process.env. Давайте воспользуемся process.env для доступа к номеру порта в файле index.js:

const express = require('express');
require('dotenv').config()
const app = express();
const port = process.env.PORT || 4000;  // Read from .env if not available then defaults to 4000

app.get('/', (req, res) => {
    res.send('Hello World!')
  })

app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}/`);
});

Теперь давайте перезапустим сервер и перейдем на localhost://3000. Вместо того, чтобы работать на 4000, наш сервер теперь работает на порту 3000, который он взял из файла .env

Вы также можете увидеть это в консоли:

Итак, вы успешно создали и сохранили файл env на своей машине.

В качестве другого примера вы можете получить доступ к DATABASE_URL следующим образом:

const dbUrl = process.env.DATABASE_URL;
// Use dbUrl to connect to your database

Вот все файлы

index.js

const express = require('express');
require('dotenv').config()
const app = express();
const port = process.env.PORT || 4000;  // Read from .env if not available then defaults to 4000

app.get('/', (req, res) => {
    res.send('Hello World!')
  })

app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}/`);
});

package.json

{
  "name": "env-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^16.3.1",
    "express": "^4.18.2"
  }
}

.env

DATABASE_URL=sample-database-url
PORT=3000
SECRET_KEY=mysecretkey

Как использовать переменные окружения в асинхронных задачах

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

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

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

Шаг 1. Настройка переменных окружения

Откройте файл .env и отредактируйте его следующим образом:

API_URL=https://jsonplaceholder.typicode.com/todos/1
API_KEY=sfjks4325
PORT=3000

Шаг 2. Использование async / await с переменными окружения

Далее установите библиотеку axios, чтобы выполнять вызовы на удаленный сервер:

npm install axios --save

Эта команда установит axios и сохранит ее как зависимость в файле package.json.

Файл package.json должен выглядеть примерно так:

{
  "name": "env-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^1.5.1",
    "dotenv": "^16.3.1",
    "express": "^4.18.2"
  }
}

Далее используйте axios для выполнения вызовов сайта jsonplaceholder и записи их в консоль.

Введите этот код в файл index.js

const express = require('express');
require('dotenv').config();
const axios = require('axios');
const app = express();
const port = process.env.PORT || 4000;  // Read from .env if not available then defaults to 4000

const fetchData = async () => {
  const url = process.env.API_URL;
  const apiKey = process.env.API_KEY;

  try {
    const response = await axios.get(url, {
      headers: { 'Authorization': `Bearer ${apiKey}` }
    });
    console.log(response.data);
  } catch (error) {
    console.error(`Error fetching data: ${error}`);
  }
};


app.get('/', (req, res) => {
  fetchData()
    res.send('Hello World!')
  })

app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}/`);
});

Что мы здесь делаем:

  1. Импортируем модули express, dotenv, axios.

  2. Затем инициализируем приложение и порт с помощью переменных окружения.

  3. Затем создаем асинхронную функцию const fetchData = async ()=> (...)

  4. В этой функции мы используем url и apiKey из файла .env. Хотя я должен отметить, что вам не нужен apiKey для вызова сайта jsonplaceholder. Я просто поместил этот ключ для демонстрации.

  5. Записываем данные в консоль.

  6. Вызываем метод fetchData() на маршруте get. Таким образом, каждый раз, когда кто-то переходит на /, вызывается метод и данные записываются в консоль.

Использование Async/await с переменными окружения в операциях с базой данных

Давайте рассмотрим еще один пример. На этот раз мы будем выполнять операции с базой данных с помощью переменных окружения.

Шаг 1: Настройка переменных окружения.

Откройте файл .env в текстовом редакторе и введите следующее:

DB_HOST=localhost
DB_PORT=5432
DB_NAME=mydatabase
DB_USER=username
DB_PASSWORD=password

Шаг 2: Установите библиотеку pg

Следующим шагом будет установка библиотеки pg:

npm install pg --save

*Шаг 3: * Подключитесь к базе данных и выполните асинхронный запрос с помощью файла .env

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

const { Pool } = require('pg');
require('dotenv').config();

const pool = new Pool({
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD
});

const fetchUsers = async () => {
  try {
    const res = await pool.query('SELECT * FROM users');
    console.log(res.rows);
  } catch (err) {
    console.error(`Error fetching users: ${err}`);
  }
};

fetchUsers();

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

Расширенные возможности работы с переменными окружения в Node JS: кодирование, валидация и преобразование типов

Переменные окружения — это не только чтение и хранение. С помощью расширенных манипуляций с переменными окружения мы узнаем о:

  • кодировании

  • валидации и

  • преобразовании типов

1. Кодирование

Кодирование используется для различных целей в переменных окружения, в том числе для обеспечения безопасности. Наиболее популярный тип кодировки — base64.

Кодирование:

const decodedData = Buffer.from(encodedData, 'base64').toString('utf-8');
console.log(`Decoded: ${decodedData}`);

2. Валидация

Валидация используется для проверки того, является ли код правильным или нет. Например, url, который вы собираетесь использовать — является ли он корректным или нет и т.д.

Или, как в этом примере, мы проверяем, находится ли номер порта в указанном диапазоне или нет.

const validatePort = (port) => {
  const parsedPort = parseInt(port, 10);
  if (isNaN(parsedPort) || parsedPort < 1024 || parsedPort > 65535) {
    throw new Error('Invalid port number');
  }
  return parsedPort;
};

const port = validatePort(process.env.PORT);

3. Преобразование типа

Переменные окружения всегда имеют тип string. Но часто возникает необходимость использовать и другие типы данных, например int или booleans. Здесь поможет преобразование типов.

Пример целого числа:

const isProduction = process.env.NODE_ENV === 'production';
const retries = parseInt(process.env.RETRIES, 10) || 3;

Пример булевого значения

const shouldLog = process.env.SHOULD_LOG === 'true';

Комбинированный пример

Вот пример, объединяющий все расширенные манипуляции с env, доступные в переменных окружения.

// Encoding and decoding
const apiKey = Buffer.from(process.env.API_KEY || '', 'base64').toString('utf-8');

// Validation
if (apiKey.length !== 32) {
  throw new Error('Invalid API key length');
}

// Type conversion
const maxUsers = parseInt(process.env.MAX_USERS, 10);
if (isNaN(maxUsers)) {
  throw new Error('MAX_USERS must be an integer');
}

Управление секретами и лучшие практики безопасности с примерами

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

1. Никогда не делайте коммит файлов .env

Всегда следите за тем, чтобы файлы .env находились в gitignore и никогда не коммитились в git-репозиторий.

# Ignore dotenv environment variables file
.env

2. Хеширование пароля

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

const bcrypt = require('bcrypt');
const saltRounds = 10;

async function hashPassword(plainPassword) {
  const hash = await bcrypt.hash(plainPassword, saltRounds);
  return hash;
}

3. Кодирование переменных окружения

В качестве меры безопасности всегда кодируйте секреты, обычно они имеют кодировку base-64.

const buffer = Buffer.from(process.env.MY_SECRET, 'base64');
const decodedSecret = buffer.toString('utf-8');

4. Централизованное управление секретами

Для крупных проектов можно рассмотреть сервисы централизованного управления секретами — например, hashicorp vault, AWS secrets manager.

Они предлагают такие расширенные функции, как ротация секретов, автоматическая аренда и ротация, а также ведение журнала аудита.

Вот пример с node vault:

const vault = require("node-vault")({
  endpoint: "https://vault-endpoint",
  token: process.env.VAULT_TOKEN,
});

async function getDatabaseCreds() {
  const data = await vault.read("path/to/secret");
  return {
    user: data.data.username,
    password: data.data.password,
  };
}

5. Принцип наименьших привилегий

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

Подводные камни

  1. Хардкодинг служебной информации

  2. Отсутствие валидации

  3. Игнорирование окружения

  4. Неадекватная безопасность

  5. Плохая документация

  6. Возврат в предыдущее значение (Fallback) по умолчанию

Нужен Chat API для вашего сайта или приложения

Поставщиком Chat API является DeadSimpleChat

  • Добавьте масштабируемый чат в приложение за считанные минуты

  • Одновременно 10 миллионов пользователей

  • Uptime 99,999% 

  • Фичи модерации

  • Чат 1-1

  • Групповой чат

  • Полностью настраиваемый

  • Чат API и SDK

  • Предварительно созданный чат

Заключение

В этой статье мы узнали о переменных окружения и о том, как их использовать в Node.js.

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

Надеюсь, вам понравилась статья, и спасибо за чтение.

Всех разработчиков на JavaScript приглашаем на открытое занятие в OTUS «Построение графических приложений с применением библиотеки Konva.js». Записаться на урок можно на странице курса.

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


  1. savostin
    11.12.2023 17:59

    По приведению типов и валидации ( а еще бы автоматической) явно не хватает Typescript. Да и может какую библиотеку кто написал?


    1. yarkov
      11.12.2023 17:59

      Так class-validator вполне подойдёт.

      import {
        IsFQDN,
        IsString,
        transformAndValidateSync,
      } from 'class-validator';
      
      class Environment {
          @IsFQDN()
          DATABASE_HOST: string;
      
          @IsString()
          DATABASE_USERNAME: string;
      
          @IsString()
          DATABASE_PASSWORD: string;
      }
      
      export default transformAndValidateSync(Environment, process.env);
      


      1. savostin
        11.12.2023 17:59

        Да, я к этому тоже пришел. Думал может чего более интересное существует...


  1. debagger
    11.12.2023 17:59

    Непонятно, как поможет защитить кодирование секретов в base64


  1. ashkraba
    11.12.2023 17:59

    Ещё бы пару слов о том, как все это добро использовать с SSR на nuxt например, было бы вообще здорово. А так огромное спасибо - было полезно.


    1. kmoseenk Автор
      11.12.2023 17:59

      Спасибо за фидбек!


  1. Rishquer
    11.12.2023 17:59

    Статья не про переменныые окружения, а про dotenv пакет и экспресс.
    Аснхронное использование, вообще непонятно о чем\зачем? Все так же синхронно получаешь внутри асинхронной функции.

    Про валидацию слишком поверхронсто. Есть смысл реализовывать интерфейс валидации, желательно на тайпскрит. Где будет удобно задавать правила для валидации например в таком виде
    HOST: envSchema.string({format: "host"});
    PORT: envSchema.number();
    FEATURE_ENABLED: envSchema.boolean()
    OPTIONAL: envSchema.string.optional()

    Валидировать все на старте приложения
    И получение переменных обернуть в класс\объект который на вход принимает ключи из описаной выше схемы и возвращает соответствующие typescript типы.
    Например
    Env.get("PORT") //выведет number
    Env.get("UNDEFINED") // если в схеме такой ключ не задан. Не должно принимать такой ключ, или возвращать тип any, первый вариант предпочтительнее.