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

Поэтому я бы полагался на объект процесса Node, чтобы извлечь значение среды и внедрить его в мой экземпляр axios в виде строкового значения, чтобы получить доступ к моему серверному API или любому другому значению в этом отношении. Но объект процесса буквально не существует в браузере, он использовался во время транспиляции и может быть настроен только во время сборки:

const instance = axios.create({
    baseURL: process.env.BACKEND_API_URL,
    timeout: 1000,
    headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    }
});

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

Слишком много хлопот для таких простых изменений, так что же вы можете сделать, чтобы обновить переменные среды, такие как BACKEND_API_URL, «динамически», без перестроения и докеризации вашего приложения?

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

...

COPY static.conf /etc/nginx/conf.d/default.conf

WORKDIR /usr/share/nginx/html
RUN apk add --no-cache bash

COPY ./configure-runtime.sh .
COPY .env .

RUN chmod +x configure-runtime.sh

CMD ["/bin/bash", "-c", "/usr/share/nginx/html/configure-runtime.sh && nginx -g \"daemon off;\""]

Важными частями вышеупомянутого Dockerfile являются введение сценария configure-runtime.sh, затем файла .env, содержащего обычные переменные среды, и, в дополнение, static.conf nginx, чтобы настроить структуру прокси, чтобы избежать CORS для конечной точки нашего API, которая может находиться в другом домене. Затем мы указываем контейнеру запустить команду CMD, которая сначала выполняет наш скрипт configure-runtime.sh, а затем запускает nginx для обслуживания нашего внешнего приложения.

Давайте посмотрим на файлы и скрипты один за другим. Начиная с файла static.conf nginx, есть {BACKEND_API_URL}заполнитель, который мы хотим динамически заменить переменными среды Docker:

server {
    listen 80 default_server;
    server_name localhost;
    charset utf-8;
    root /usr/share/nginx/html;

    location /api {
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass {BACKEND_API_URL};
    }

    location / {
        try_files $uri $uri/ /index.html =404;
    }

}

У нас также есть файл .env. Мы будем использовать его как «карту» и запасной вариант на случай, если наш скрипт configure-runtime не сможет найти какие-либо установленные переменные среды Docker, и вместо этого будем использовать значения файла .env:

BACKEND_API_URL=http://localhost

Затем наш скрипт configure-runtime.sh выполняет остальную работу. Он анализирует все переменные среды Docker, которые мы определили, и создает файл javascript (runtime-env.js) с переменными, назначенными в качестве свойств глобального window object. Именно так наше веб-приложение может получить доступ к этим переменным глобально через браузер и объект окна:

#!/bin/bash

# Assign the nginx configuration filename
nginx_conf="/etc/nginx/conf.d/default.conf"

# Recreate runtime-env config file
rm -rf ./runtime-env.js && touch ./runtime-env.js

echo "window._env_ = {" >> ./runtime-env.js

# Read each line in .env file, each line represents key=value pairs
while read -r line || [[ -n "$line" ]];
do
  # Split env variables by character `=`
  if printf '%s\n' "$line" | grep -q -e '='; then
    varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
    varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
  fi

  # Read value of current variable if exists as Environment variable
  value=$(printf '%s\n' "${!varname}")

  # Otherwise use value from .env file
  [[ -z $value ]] && value=${varvalue}
  
  # Append configuration property to JS file
  echo "  $varname: \"$value\"," >> ./runtime-env.js

  # Replace the nginx environment variable placeholder
  if [[ $varname != "" && $value != "" ]]; then
    sed -i 's|{'$varname'}|'"$value"'|g' $nginx_conf
  fi

done < .env

echo "}" >> ./runtime-env.js

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

Наконец, мы включаем файл runtime-env.js, сгенерированный на странице index.html нашего приложения, и меняем атрибут baseURL axios непосредственно на наш API, как показано ниже (подсказка: использование прокси-сервера, такого как nginx , вы бы хотели, чтобы это было просто «/api», чтобы избежать запросов, связанных с разными источниками):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <script src="runtime-env.js"></script>
  </head>
  <body id="page-top">
    <div id="app"></div>
  </body>
</html>
const instance = axios.create({
    baseURL: window._env_.BACKEND_API_URL, // process.env.BACKEND_API_URL,
    timeout: 1000,
    headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    }
});

Действие!

Используя docker-compose файлы yaml и .env, как показано ниже:

# docker-compose.yaml
version: "3.2"
services:
  frontend:
    image: frontend:1.0
    ports:
      - "8001:80"
    environment:
      - "BACKEND_API_URL=http://localhost:4567/api"
      - "DUMMY_VAR_1=some-value-1"
      - "DUMMY_VAR_2=some-value-2"

# .env file
BACKEND_API_URL=http://localhost
DUMMY_VAR_1=1234
DUMMY_VAR_2=5678

Давайте проверим конфигурацию nginx для нашего работающего образа контейнера, чтобы увидеть, правильно ли заменена наша директива proxy_pass:

Давайте также проверим исходный код нашей веб-страницы, чтобы увидеть набор объектов окна без конечной точки внутреннего API и фиктивных переменных:

Все вышеперечисленные значения доступны в вашем внешнем коде с помощью window._env_., за которым следует имя переменной.

Те же идеи можно использовать для Angular, обновив файл environment.ts и получив эти значения с помощью файла runtime-env.js:

export const environment = {
  apiUrl: window["_env_"]["BACKEND_API_URL"],
  ...
};

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