From 75d149d36247336bbae3a5a419547d63b467a9a6 Mon Sep 17 00:00:00 2001 From: Andrey Potyomkin Date: Thu, 6 Jun 2024 14:57:06 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=9E=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=20?= =?UTF-8?q?guide=20=D0=BF=D0=BE=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B5=20?= =?UTF-8?q?=D1=81=20env=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ENV. Описано Intro * feat: ENV. Описано Arch * feat: ENV. Начало csr * feat: ENV. Написано про безопасность * feat: ENV. Закончена дока по CSR * feat: ENV. Закончена дока по SSR * feat: ENV. Дополнена дока про хэш * feat: ENV. Дополнена инфа про d.ts * feat: ENV. Исправлены грамматические ошибки * feat: Изменен процесс генерации env --- .cspell-ignore | 7 ++ .cspell.json | 2 +- docs/env/_category_.json | 8 ++ docs/env/arch.md | 19 ++++ docs/env/csr.md | 182 +++++++++++++++++++++++++++++++++++++++ docs/env/intro.md | 9 ++ docs/env/safety.md | 24 ++++++ docs/env/ssr.md | 9 ++ 8 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 docs/env/_category_.json create mode 100644 docs/env/arch.md create mode 100644 docs/env/csr.md create mode 100644 docs/env/intro.md create mode 100644 docs/env/safety.md create mode 100644 docs/env/ssr.md diff --git a/.cspell-ignore b/.cspell-ignore index 5f9476f..cf303b0 100644 --- a/.cspell-ignore +++ b/.cspell-ignore @@ -359,3 +359,10 @@ jsdoc дебагинга админские Permissioning +vite +хэшу +неэкспортируемых +WORKDIR +fholzer +brotli +envsubst diff --git a/.cspell.json b/.cspell.json index 087642f..75b9832 100644 --- a/.cspell.json +++ b/.cspell.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", "version": "0.2", "language": "en,ru", - "languageId": "ts,tsx", + "languageId": "ts,tsx,markdown,docker,bash", "import": [ "@cspell/dict-ru_ru/cspell-ext.json" ], diff --git a/docs/env/_category_.json b/docs/env/_category_.json new file mode 100644 index 0000000..1050c83 --- /dev/null +++ b/docs/env/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Работа с env", + "position": 2, + "link": { + "type": "generated-index", + "description": "Работа с env переменными в приложениях" + } +} diff --git a/docs/env/arch.md b/docs/env/arch.md new file mode 100644 index 0000000..c715e0a --- /dev/null +++ b/docs/env/arch.md @@ -0,0 +1,19 @@ +--- +sidebar_position: 1 +--- + +# Работа с env в архитектуре + +В зависимости от сборщика или фреймворка, env переменные доставляются в приложение разным способом: +- В vite через `import.meta.env` +- В webpack через `process.env` +- В нашем подходе для CSR через `window.__ENV__` + +Все обращение к источнику env должны происходить только на уровне [application слоя](https://industrious-search-cdf.notion.site/Application-cc27fa7727fd49599caafc2f2d76ae23). +Работа с env только на уровне application позволяет не зависеть приложению от сборщика или механизма доставки env. + +## ConfigService + +Слои приложения, отличные от **application**, должны получать данные из env через `ConfigService`. + +Подробнее о ConfigService читайте в [документации](https://industrious-search-cdf.notion.site/Config-cbfc6d248c5f47cd80d35f0f2cc95281). diff --git a/docs/env/csr.md b/docs/env/csr.md new file mode 100644 index 0000000..f1a8290 --- /dev/null +++ b/docs/env/csr.md @@ -0,0 +1,182 @@ +--- +sidebar_position: 3 +--- + +# CSR + Nginx. Динамические env + +В данном разделе описана инструкция загрузки env переменных при использовании CSR и Nginx для раздачи статики. + +## Пример + +Пример реализован в [vite-boilerplate](https://github.com/kaluga-astral/vite-boilerplate/tree/main). + +## Проблема получения env в build time + +Получение env в build time - это классический подход. + +Работает следующим образом: +1. Сборщик при запуске получает +2. Либо помещает полученные env в `import.meta.env` (vite), либо делает replace `process.env` переменных на их значения непосредственно в js коде (webpack) +3. Переменные доступны в браузере при выполнении js + +Проблемы: +- Для изменения значений env необходимо заново пересобирать приложение +- Если в приложении есть поддержка white label, то для каждого бренда необходимо собирать отдельный docker image. +Это сильно увеличивает время сборки приложения и количество ресурсов, необходимое для поддержки и развертывания + +Для решения описанных проблем необходимо делать inject env в html при запуске docker контейнера. + +## Генерация `index.html` и inject env + +Для того чтобы при изменении env переменных не приходилось заново делать сборку, необходимо при запуске docker контейнера генерировать `index.html` и инжектировать в html env переменные. + +### Генерация `index.html` из `index.template.html` + +В репозитории должен находиться только `index.template.html` - это шаблон html, на основе которого будет построен `index.html`. + +`index.html` должен быть добавлен в `.gitignore`. + +#### Содержимое `index.template.html` + +**[Пример index.template.html](https://github.com/kaluga-astral/vite-boilerplate/blob/main/index.template.html)** + +`index.template.html` содержит всю необходимую для приложения разметку и ` +``` + +#### Зачем нужен `index.template.html` и почему `index.html` в `.gitignore`? + +При запуске dev-server должно происходить inject env переменных в `index.html`, +если файл не будет в `.gitignore`, то в репозиторий могут случайно попасть изменения, которых разработчик не делал. + + +#### Для генерации и inject env используются bash скрипты. + +Bash используется по причине того, что для CSR в компании используется Nginx. +Это означает, что в итоговом docker контейнере нет nodejs, а если добавить в образ nodejs, то он вырастет в разы по объему, что повлияет на скорость сборок и оптимизацию хранилища. + +## Dev Mode + +### Генерация `index.html`. `generateEnv.dev.sh` скрипт + +**[Пример generateEnv.dev.sh](https://github.com/kaluga-astral/vite-boilerplate/tree/main/scripts/generateEnv.dev.sh)** + +В режиме разработки необходимо, чтобы перед каждым запуском dev-server выполнялся bash скрипт, который: +1. Парсит все переменные из файла `.env.local` (или `.env.dev`) с префиксами `PUBLIC_` +2. Создает в директории для раздачи статики `index.html` с копией содержимого из `index.template.html`. +Для vite директория для раздачи статики - это корень приложения. Для webpack - public директория +3. Заменяет в `index.html` `window.__ENV__={}` на: +```js +window.__ENV__={"PUBLIC_API_URL":"https://astral.ru"}; +``` + +## Prod Mode + +### Генерация `index.html` + +#### Dockerfile + +При запуске docker контейнера необходимо запускать bash скрипт, который сгенерирует `index.html` файл и инжектирует в него env. +Для этого в `Dockerfile` необходимо добавить команду выполнения скрипта при запуске контейнера: +```dockerfile +FROM node:22-alpine AS build + +WORKDIR /usr/src/app + +COPY package.json package-lock.json* ./ + +COPY . . + +RUN npm i --production + +RUN npm run build + +FROM fholzer/nginx-brotli:v1.19.1 + +COPY .nginx/nginx.conf.template /etc/nginx/nginx.conf.template +COPY --from=build /usr/src/app/dist /usr/share/nginx/html + +# Запускаем контейнер при помощи exec в shell оболочке, чтобы иметь доступ к env +ENTRYPOINT ["sh", "/usr/share/nginx/html/scripts/startup.prod.sh"] +``` + +#### Скрипт `startup.prod.sh` + +**[Пример startup.prod.sh](https://github.com/kaluga-astral/vite-boilerplate/tree/main/scripts/startup.prod.sh)** + +Скрипт `startup.prod.sh` выполняет следующие действия: +1. Достает из текущего окружения все env переменные с префиксом `PUBLIC_` +2. Создает `index.html` с копией содержимого из `index.template.html` +3. Заменяет в `index.html` `window.__ENV__={}` на: +```js +window.__ENV__={"PUBLIC_API_URL":"https://astral.ru"}; +``` +5. Подменяет переменные для nginx на их значения. Читай об этом подробнее [здесь](#подстановка-переменных-в-nginx) +6. Запускает nginx + +### Подстановка переменных в nginx + +В зависимости от env в проекте может изменяться `nginx.conf`. + +Для того чтобы nginx.conf менялся при запуске необходимо: +1. Создать в проекте файл `nginx.conf.template`. Из него будет генерироваться уже настоящий `nginx.conf`. +2. Заменить динамические участки конфига на `${PUBLIC_WS_URL}`. Пример: +``` +http { + ... + + server { + listen 80; + listen [::]:80; + + ... + + add_header Content-Security-Policy connect-src 'self' ${PUBLIC_WS_URL}; + + ... + } +} +``` +3. Добавить в `startup.prod.sh` следующий код с переменными, которые необходимо подменить: +```bash +# Необходимо экспортировать, тк envsubst является разветвленным процессом и не знает неэкспортируемых переменных +export PUBLIC_WS_URL + +# Подмена указанных переменных в nginx.conf.template и копирование всего файла в nginx.conf +envsubst "${PUBLIC_WS_URL}" < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf +``` +4. При запуске `startup.prod.sh` подменит в `nginx.conf.template` все переменные на их значения и создаст `nginx.conf`, который и будет использовать для запуска nginx + +## Использование env из сгенерированного файла + +Для использования переменных необходимо определить тип для `window.__ENV__` в `global.d.ts`: +```ts +interface Window { + __ENV__: { + PUBLIC_API_URL: string; + PUBLIC_SENTRY_DSN: string; + PUBLIC_SENTRY_ENV: string; + PUBLIC_RELEASE_TAG: string; + }; +} +``` + +В браузере доступ к env переменным осуществляется через `window.__ENV__` в application слое приложения: +```ts +configService.init({ + apiUrl: window.__ENV__.PUBLIC_API_URL, + monitoringDsn: window.__ENV__.PUBLIC_SENTRY_DSN, + monitoringStand: window.__ENV__.PUBLIC_SENTRY_ENV, + monitoringRelease: window.__ENV__.PUBLIC_RELEASE_TAG, +}); +``` + +## Преимущества inject env в `index.html` + +Inject env позволяет: +- Получать env в браузере синхронно. Нет необходимости делать обработку асинхронного получения данных. Код приложения гарантированно получает все env на этапе выполнения +- Снизить к 0 затраты на загрузку динамических env diff --git a/docs/env/intro.md b/docs/env/intro.md new file mode 100644 index 0000000..fc10cbf --- /dev/null +++ b/docs/env/intro.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 0 +--- + +# Intro + +Раздел содержит: +- Как работать с env в рамках единой архитектуры +- Как генерировать и загружать env в CSR (Client Side Rendering) diff --git a/docs/env/safety.md b/docs/env/safety.md new file mode 100644 index 0000000..9ac6003 --- /dev/null +++ b/docs/env/safety.md @@ -0,0 +1,24 @@ +--- +sidebar_position: 2 +--- + +# Безопасность. Использование PUBLIC и PRIVATE env + +Для env переменных, которые должны быть доступны в браузере, необходимо указывать префикс `PUBLIC_`: +- `PUBLIC_API_URL` +- `PUBLIC_BRAND` + +Для env переменных, которые должны быть доступны только на стадии билда, необходимо указывать префикс `PRIVATE_`: +- `PRIVATE_RELEASE_MANAGER_TOKEN` + +## Мотивация + +Если все env переменные сделать доступные в браузере, то злоумышленники могут найти уязвимости системы потому, что env хранит информацию о сервере, на котором запускает билд или само приложение. + +Именно по этой причине Nextjs и Vite требуют для env переменных префиксы: `NEXT_PUBLIC | NEXT_PRIVATE`, `VITE_`. + +## CSR + +Если приложение собирается через `webpack`, то достаточно просто следовать указанным ранее соглашениям по именованию. + +Если приложение собирается через `vite`, то необходимо использовать [envPrefix](https://vitejs.dev/config/shared-options.html#envprefix) для кастомизации префикса. diff --git a/docs/env/ssr.md b/docs/env/ssr.md new file mode 100644 index 0000000..8f8ae46 --- /dev/null +++ b/docs/env/ssr.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 4 +--- + +# Nextjs. SSR и SSG + +Если на проекте используется Nextjs, то необходимо использовать тот механизм работы с env, который предоставляет фреймворк. + +При SSR и SSG env нужны при билде.