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

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

В этом сообщении мы предполагаем, что вы уже знакомы с инструментом Manifold CLI и можете настраивать свои ресурсы. Мы также создали демонстрационное приложение с React, чтобы вы могли следить за ним.

Обслуживание статических файлов в контейнере

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

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

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

Сборка статических файлов

Во-первых, нам нужно сгенерировать наши статические файлы. Это можно сделать и через Докер. В качестве примера у нас есть очень простое приложение React, которое просто обслуживает Hello World! пример.

Для создания наших файлов мы можем использовать Yarn следующим образом:

manifold run yarn build

Это создаст все наши статические файлы в соответствии с нашей конфигурацией и поместит статические файлы в каталог build/. Наши переменные среды будут введены с помощью команды manifold run и помещены в строку. Эти скомпилированные файлы - это то, что nginx будет служить нашей аудитории.

Чтобы это сработало, нам нужно настроить наш .env файл для чтения из нашей среды:

REACT_APP_STRIPE_KEY=${STRIPE_KEY}
REACT_APP_SEGMENT_KEY=${SEGMENT_KEY}

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

Создание контейнера производственного уровня

Следующим шагом является создание производственного контейнера, который позволит обслуживать наше приложение. Мы будем использовать nginx для обслуживания нашего приложения, и есть готовый образ nginx для Docker, который мы можем использовать для создания собственного образа. Во-первых, нам нужно создать файл конфигурации nginx, который знает, как обслуживать наши статические файлы. В корневом каталоге создайте файл nginx.conf.

server {
    listen 80;
    server_name _;
    root /var/www/;
    index index.html;
    # Force all paths to load either itself (js files) or go through index.html.
    location / {
        try_files $uri /index.html;
    }
}

Это очень упрощенная конфигурация, которая сначала пытается сопоставить запрошенный URL-адрес с файлом в каталоге /var/www/. Если этот файл недоступен, он будет обслуживать index.html, который, в свою очередь, будет знать, как обрабатывать маршруты и т. Д. Через базовое приложение JS.

Чтобы создать наш контейнер nginx, мы можем предоставить Dockerfile.

FROM nginx:stable
COPY ./build/ /var/www
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
CMD ["nginx -g 'daemon off;'"]

Мы также можем использовать Docker для создания статических файлов и использовать многоступенчатую функциональность для определения всего этого в одном Dockerfile.

FROM node AS build
WORKDIR /app
COPY . .
RUN yarn build
FROM nginx:stable
COPY — from=build /app/build/ /var/www
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
CMD ["nginx -g 'daemon off;'"]

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

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

docker build -t manifoldco/demo-app:latest .

Обратной стороной этого изображения является то, что оно зависит от среды. Когда мы создали наши статические файлы с manifold run yarn build, мы получили учетные данные для конкретной среды через manifold-cli и внедрили их в наши статические файлы. Это означает, что мы не можем использовать это конкретное изображение для какой-либо другой среды.

Делаем ваши статические файлы динамическими

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

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

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

cat .env | grep = | sort | sed -e 's|REACT_APP_\([a-zA-Z_]*\)=\(.*\)|REACT_APP_\1=NGINX_REPLACE_\1|' > .env.local

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

Теперь мы можем использовать sub_filter функциональность Nginx в сочетании с envsubst. Мы воспользуемся функцией sub_filter в нашем файле конфигурации, чтобы сообщить конфигурации nginx: «Если эта строка присутствует в любом из наших файлов JS, замените ее этим новым значением». Мы снова воспользуемся инструментом sed, чтобы сделать возможным динамическое создание этого файла конфигурации из ключей, имеющихся в нашем проекте многообразия.

location ~* ^.+\.js$ {
    LOCATION_SUB_FILTER
    sub_filter_once off;
    sub_filter_types *;
}

Если мы поместим это в наш nginx.conf.sample файл, мы сможем использовать sed для замены LOCATION_SUB_FILTER для настройки переменных среды.

Во-первых, мы должны отформатировать переменные среды в форматеsub_filter.

export NGINX_SUB_FILTER=$(cat .env | grep '=' | sort | sed -e 's/REACT_APP_\([a-zA-Z_]*\)=\(.*\)/sub_filter\ \"NGINX_REPLACE_\1\" \"$$\{\1\}\";/')

Теперь мы можем поместить это в наш nginx.conf файл.

cat nginx.conf.sample | sed -e "s|LOCATION_SUB_FILTER|$(echo $NGINX_SUB_FILTER)|" | sed 's|}";\ |}";\n\t\t|g'

Это сгенерирует файл конфигурации, в котором настроено множество sub_filter элементов.

location ~* ^.+\.js$ {
    sub_filter "NGINX_REPLACE_SEGMENT_KEY" "${SEGMENT_KEY}";
    sub_filter "NGINX_REPLACE_STRIPE_KEY" "${STRIPE_KEY}";
    sub_filter_once off;
    sub_filter_types *;
}

Теперь мы можем использовать инструмент envsubst, чтобы запустить nginx с правильными ключами. Таким образом, envsubst посмотрит на предоставленный файл и увидит, где мы хотим заменить переменные среды. Если он найдет совпадения, он заменит то, что предусмотрено в среде. Это позволяет нам внедрять ENV в наш контейнер, который будет загружен в наши статические файлы. Это также открывает возможность добавить дополнительную конфигурацию в наш nginx.conf.sample файл.

server {
    listen ${PORT};
    server_name ${SERVER_NAME};
    root /var/www/;
    index index.html;
    location ~* ^.+\.js$ {
        LOCATION_SUB_FILTER
        sub_filter_once off;
        sub_filter_types *;
    }
    location / {
        try_files $uri /index.html;
    }
}

Для этого нам нужно запустить наш контейнер докеров, предварительно запустив envsubst. Наш Dockerfile отразит это изменение.

FROM nginx:stable
COPY -from=build /app/build/ /var/www
RUN rm /etc/nginx/conf.d/default.conf
COPY ./nginx.conf /etc/nginx/conf.d/default.conf.template
# This is a hack around the envsubst nginx config. Because we have `$uri` set
# up, it would replace this as well. Now we just reset it to its original value.
ENV uri \$uri
# Default configuration
ENV PORT 80
ENV SERVER_NAME _
CMD [“sh”, “-c”, “envsubst < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g ‘daemon off;’”]

Теперь ваш контейнер не зависит от среды, что означает, что он не знает конкретных деталей среды. Это означает, что теперь вы можете использовать другой STRIPE_KEY или SEGMENT_KEY для своей среды test, staging и production без необходимости перекомпилировать файлы и создавать новый образ Docker.

Зачем использовать Nginx для замены значений?

Может возникнуть один вопрос: зачем использовать nginx для замены строки, а не заменять ее в самом файле с помощью envsubst?

Мы выбрали этот подход по двум причинам. Первая причина заключается в том, что при встраивании ENV в качестве переменной среды во время сборки возникают проблемы. Используемый нами пакет dotenv заменяет значения $, { и } на “”. Это приводит к тому, что наше замененное значение приводит только к ключевому значению.

Этого можно избежать, заменив эти значения в контейнере префиксом $. Здесь возникает вторая проблема. В нашей кодовой базе есть такие фрагменты, как $t и $e. Это не переменные среды, поэтому их не следует заменять, однако envsubst заменит их. Чтобы не настраивать исключения для каждой такой переменной, а в будущем, возможно, и для других, мы решили позволить nginx справиться с этим.

Запуск вашего контейнера

Внесение всего в этот образ позволяет легко запускать ваш проект где угодно. Будь то локально, на CI или в вашей производственной среде. Чтобы проиллюстрировать это, мы воспользуемся docker-compose, чтобы запустить контейнер, и с помощью manifest-cli, чтобы ввести учетные данные.

Во-первых, нам нужно создать docker-compose.yml файл, который описывает, как мы хотим запускать нашу службу.

version: '3'
services:
  dashboard:
    image: manifoldco/demo-app
    ports:
      - 3001:3001
    environment:
      - PORT=${PORT:-3001}
      - STRIPE_KEY=${STRIPE_KEY}
      - SEGMENT_KEY=${SEGMENT_KEY}

Теперь мы можем запустить наш контейнер с помощью manifold run -p frontend-application docker-compose up. Это приведет к внедрению всех учетных данных, установленных в нашем проекте frontend-application на Manifold, в наш контейнер.

Разработка и развертывание

Теперь, когда у вас есть контейнер, готовый к производству, остается 2 вопроса. Что делать для разработки и как развернуть этот контейнер?

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

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