Генерируем и раздаём ассеты

Прямо сейчас мы пишем новые главы самоучителя!
Эта страница находится в разработке.

Показать, что уже готово

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

Подготовка окружения

Подготовьте рабочее окружение согласно инструкциями главы “Подготовка окружения”, если это ещё не сделано.

Рабочее окружение работало, но перестало? Инструкции из этой главы не работают? Может помочь:

Работает ли Docker?

Запустим приложение Docker Desktop. Приложению понадобится некоторое время для того, чтобы запустить Docker. Если никаких ошибок в процессе запуска не возникло, то проверим, что Docker запущен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Запустим приложение Docker Desktop. Приложению понадобится некоторое время для того, чтобы запустить Docker. Если никаких ошибок в процессе запуска не возникло, то проверим, что Docker запущен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Запустим Docker:

sudo systemctl restart docker

Убедимся, что Docker запустился:

sudo systemctl status docker

Результат выполнения команды, если Docker успешно запущен:

● docker.service - Docker Application Container Engine
     Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2021-06-24 13:05:17 MSK; 13s ago
TriggeredBy: ● docker.socket
       Docs: https://docs.docker.com
   Main PID: 2013888 (dockerd)
      Tasks: 36
     Memory: 100.3M
     CGroup: /system.slice/docker.service
             └─2013888 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

dockerd[2013888]: time="2021-06-24T13:05:16.936197880+03:00" level=warning msg="Your kernel does not support CPU realtime scheduler"
dockerd[2013888]: time="2021-06-24T13:05:16.936219851+03:00" level=warning msg="Your kernel does not support cgroup blkio weight"
dockerd[2013888]: time="2021-06-24T13:05:16.936224976+03:00" level=warning msg="Your kernel does not support cgroup blkio weight_device"
dockerd[2013888]: time="2021-06-24T13:05:16.936311001+03:00" level=info msg="Loading containers: start."
dockerd[2013888]: time="2021-06-24T13:05:17.119938367+03:00" level=info msg="Loading containers: done."
dockerd[2013888]: time="2021-06-24T13:05:17.134054120+03:00" level=info msg="Daemon has completed initialization"
systemd[1]: Started Docker Application Container Engine.
dockerd[2013888]: time="2021-06-24T13:05:17.148493957+03:00" level=info msg="API listen on /run/docker.sock"

Теперь проверим, что Docker доступен и корректно настроен:

docker run hello-world

Результат успешного выполнения команды:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:9f6ad537c5132bcce57f7a0a20e317228d382c3cd61edae14650eec68b2b345c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

При возникновении проблем обратитесь к документации Docker для их устранения.

Перезагружали компьютер после подготовки окружения?

Запустим кластер minikube, уже настроенный в начале главы “Подготовка окружения”:

minikube start

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

kubectl config set-context minikube --namespace=werf-guide-app

Результат успешного выполнения команды:

😄  minikube v1.20.0 on Ubuntu 20.04
✨  Using the docker driver based on existing profile
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🎉  minikube 1.21.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.21.0
💡  To disable this notice, run: 'minikube config set WantUpdateNotification false'

🔄  Restarting existing docker container for "minikube" ...
🐳  Preparing Kubernetes v1.20.2 on Docker 20.10.6 ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/google_containers/kube-registry-proxy:0.4
    ▪ Using image k8s.gcr.io/ingress-nginx/controller:v0.44.0
    ▪ Using image registry:2.7.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying registry addon...
🔎  Verifying ingress addon...
🌟  Enabled addons: storage-provisioner, registry, default-storageclass, ingress
🏄  Done! kubectl is now configured to use "minikube" cluster and "werf-guide-app" namespace by default

Убедитесь, что вывод команды содержит строку:

Restarting existing docker container for "minikube"

Если её нет, значит был создан новый кластер minikube вместо использования старого. В таком случае повторите установку рабочего окружения с minikube с самого начала.

Теперь запустите команду в фоновом PowerShell-терминале и не закрывайте его:

minikube tunnel --cleanup=true

Запустим кластер minikube, уже настроенный в начале главы “Подготовка окружения”:

minikube start --namespace werf-guide-app

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

kubectl config set-context minikube --namespace=werf-guide-app

Результат успешного выполнения команды:

😄  minikube v1.20.0 on Ubuntu 20.04
✨  Using the docker driver based on existing profile
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🎉  minikube 1.21.0 is available! Download it: https://github.com/kubernetes/minikube/releases/tag/v1.21.0
💡  To disable this notice, run: 'minikube config set WantUpdateNotification false'

🔄  Restarting existing docker container for "minikube" ...
🐳  Preparing Kubernetes v1.20.2 on Docker 20.10.6 ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/google_containers/kube-registry-proxy:0.4
    ▪ Using image k8s.gcr.io/ingress-nginx/controller:v0.44.0
    ▪ Using image registry:2.7.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image docker.io/jettech/kube-webhook-certgen:v1.5.1
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying registry addon...
🔎  Verifying ingress addon...
🌟  Enabled addons: storage-provisioner, registry, default-storageclass, ingress
🏄  Done! kubectl is now configured to use "minikube" cluster and "werf-guide-app" namespace by default

Убедитесь, что вывод команды содержит строку:

Restarting existing docker container for "minikube"

Если её нет, значит был создан новый кластер minikube вместо использования старого. В таком случае повторите установку рабочего окружения с minikube с самого начала.

Случайно удаляли Namespace приложения?

Если вы непреднамеренно удалили Namespace приложения, то необходимо выполнить следующие команды, чтобы продолжить прохождение руководства:

kubectl create namespace werf-guide-app
kubectl create secret docker-registry registrysecret \
  --docker-server='https://index.docker.io/v1/' \
  --docker-username='<имя пользователя Docker Hub>' \
  --docker-password='<пароль пользователя Docker Hub>'

Результат успешного выполнения команды:

namespace/werf-guide-app created
secret/registrysecret created
Ничего не помогло, окружение или инструкции по-прежнему не работают?

Если ничего не помогло, то пройдите главу “Подготовка окружения” с начала, подготовив новое окружение с нуля. Если и это не помогло, тогда, пожалуйста, расскажите о своей проблеме в нашем Telegram или оставьте Issue на GitHub, и мы обязательно вам поможем.

Подготовка репозитория

Обновим существующий репозиторий с приложением:

Выполним следующий набор команд в PowerShell:

cd ~/werf-guide/app

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -Recurse -Force ~/werf-guide/guides//* .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Выполним следующий набор команд в Bash:

cd ~/werf-guide/app

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -rfT ~/werf-guide/guides/ .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Не работает? Попробуйте инструкции на вкладке “Начинаю проходить руководство с этой главы” выше.

Подготовим новый репозиторий с приложением:

Выполним следующий набор команд в PowerShell:

# Склонируем репозиторий с примерами в ~/werf-guide/guides, если он ещё не был склонирован
if (-not (Test-Path ~/werf-guide/guides)) {
  git clone https://github.com/werf/werf-guides $env:HOMEPATH/werf-guide/guides
}

# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app
rm -Recurse -Force ~/werf-guide/app
cp -Recurse -Force ~/werf-guide/guides/ ~/werf-guide/app

# Сделаем из директории ~/werf-guide/app git-репозиторий
cd ~/werf-guide/app
git init
git add .
git commit -m initial

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -Recurse -Force ~/werf-guide/guides//* .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Выполним следующий набор команд в Bash:

# Склонируем репозиторий с примерами в ~/werf-guide/guides, если он ещё не был склонирован
test -e ~/werf-guide/guides || git clone https://github.com/werf/werf-guides ~/werf-guide/guides

# Скопируем файлы приложения (пока без изменений) в ~/werf-guide/app
rm -rf ~/werf-guide/app
cp -rf ~/werf-guide/guides/ ~/werf-guide/app

# Сделаем из директории ~/werf-guide/app git-репозиторий
cd ~/werf-guide/app
git init
git add .
git commit -m initial

# Чтобы увидеть, какие изменения мы собрались вносить далее в этой главе, заменим все файлы приложения
# в репозитории новыми, уже измененными файлами приложения, которые содержат описанные далее изменения.
git rm -r .
cp -rfT ~/werf-guide/guides/ .
git add .
git commit -m WIP
Посмотреть, какие именно изменения мы произведём
# Показать, какие файлы мы собираемся изменить
git show --stat
# Показать изменения
git show

Файлы, упомянутые в главе

  • .helm/templates/deployment.yaml
  • .helm/templates/service.yaml
  • .helm/templates/ingress.yaml
  • .helm/templates/configmap.yaml
  • .werf/nginx.conf
  • frontend/src/config/env.json
  • frontend/src/index.html
  • frontend/src/index.js
  • frontend/package.json
  • frontend/package-lock.json
  • frontend/webpack.config.js
  • Dockerfile.frontend
  • werf.yaml

В какой-то момент в процессе развития вашего базового приложения вам понадобятся ассеты (т.е. картинки, CSS, JS…).

Для того, чтобы обработать ассеты, воспользуемся webpack - это гибкий в плане реализации ассетов инструмент. Настраивается его поведение в webpack.config.js и package.json.

Реализовать сборку сгенерированных ассетов можно двумя способами:

Как правильно сделать выбор?

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

  • Как часто вносятся изменения в код, относящийся к каждому образу. Например, если изменения почти всегда вносятся одновременно и в статику, и в приложение — меньше смысла отделять их сборку: ведь всё равно оба пересобирать.
  • Размер полученных образов и, как следствие, объём данных, которые придётся скачивать при каждом перевыкате.

Сравните два сценария:

  • Над кодом работают fullstack-разработчики и они привыкли коммитить, когда всё уже готово. Следовательно, пересборка обычно затрагивает и frontend, и backend. Собранные по отдельности образы — 100 МБ и 600 МБ, а вместе - 620 МБ. Очевидно, стоит использовать один образ.
  • Над кодом работают отдельно frontend и backend или же коммиты забрасываются и выкатываются часто (даже после мелких правок только в одной из частей кода). Собранные по отдельности образы сравнительно одинаковые: 100 МБ и 150 МБ, а вместе — 240 МБ. Если выкатывать вместе, то всегда будут пересобираться оба образа и выкатываться по 240 МБ, а если отдельно — то не более 150 МБ.

Мы сделаем два отдельных образа.

Подготовка к внесению изменений

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

В традиции фронтенд-разработки сложилась обратная практика: пробрасывать некоторые переменные, завязанные на окружение, на стадии сборки.

Так что делать с legacy-проектами?

Мы понимаем, что существует огромное количество приложений, в которых конфигурация задаётся на стадии сборки. Однако решение проблемы legacy-проектов выходит за рамки этого самоучителя: мы рассматриваем задачу Kubernetes’ации на приложении, где такой проблемы нет.

Конечно, лучше начать переписывать legacy раньше, чем позже. Но мы понимаем, что это дорогая и сложная задача.

В качестве временной меры иногда можно подставить вместо значения переменных, завязанных на окружение, уникальную строку. К примеру, если в приложение передаётся домен CDN-сервера cdn_server со значениями mycdn0.example.com / mycdn0-staging.example.com — можно вместо этих значений в сборку передать случайный GUID cdfe0513-ba1f-4f92-8503-48a497d98059. А в Helm-шаблонах, в init-контейнере, сделать с помощью утилиты sed замену GUID’а на нужное именно на этом окружении значение.

Но использование этого или другого «костыля» является лишь временной мерой с сомнительным результатом и не избавляет от необходимости модернизировать JS-приложение.

Код фронтенда

С исходным кодом фронтенд-приложения, которое мы будем использовать, можно ознакомиться в репозитории. Оно будет состоять из html файла, который будет подгружать JS-скрипты. Те, в свою очередь, при инициализации будут подгружать переменные окружения из файла /config/env.json и, затем, получать список лейблов из API.

Код приложения подробнее

Код html-страницы:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>My app</title>
</head>
<body>
<h1>Hello world!</h1>
<p>This is JS app.</p>
<p><strong>List of labels from backend: </strong><span id="content"></span></p>
<p><strong>Link from config: </strong> <a href="#" id="url">link</a></p>
</body>
</html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>My app</title> </head> <body> <h1>Hello world!</h1> <p>This is JS app.</p> <p><strong>List of labels from backend: </strong><span id="content"></span></p> <p><strong>Link from config: </strong> <a href="#" id="url">link</a></p> </body> </html>

JS, отображающий список лейблов и ссылку, зависящую от стенда, на который выкачено приложение:

var request = new XMLHttpRequest();
request.open('GET', '/config/env.json', false);  // `false` makes the request synchronous
request.send(null);

if (request.status === 200) {
    const variables = JSON.parse(request.responseText);
    document.getElementById("url").href = variables.url;

    // Business logic here
    console.log('It works');
    var request_content = new XMLHttpRequest();
    request_content.open('GET', '/api/labels', false);  // `false` makes the request synchronous
    request_content.send(null);
    if (request_content.status === 200) {
        document.getElementById("content").innerHTML = request_content.responseText;
    } else {
        document.getElementById("content").innerHTML = "sorry, error while loading";
    }
}
var request = new XMLHttpRequest(); request.open('GET', '/config/env.json', false); // `false` makes the request synchronous request.send(null); if (request.status === 200) { const variables = JSON.parse(request.responseText); document.getElementById("url").href = variables.url; // Business logic here console.log('It works'); var request_content = new XMLHttpRequest(); request_content.open('GET', '/api/labels', false); // `false` makes the request synchronous request_content.send(null); if (request_content.status === 200) { document.getElementById("content").innerHTML = request_content.responseText; } else { document.getElementById("content").innerHTML = "sorry, error while loading"; } }

Файл env.json, который при выкате на каждый стенд будет подменяться на актуальную для этого стенда версию (о том, как это будет происходить мы поговорим ниже):

{
  "url": "http://defaultvalue.op"
}
{ "url": "http://defaultvalue.op" }

Код сборки этого приложения Webpack-ом (обратите внимание, что файл env.json просто копируется и никак не применяется!):

const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require("path");

module.exports = {
    plugins: [
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, "src", "index.html"),
        }),
        new CopyWebpackPlugin({
            patterns: [
                { from: "src/config", to: 'config' }
            ]
        }),
    ],
};
const HtmlWebpackPlugin = require("html-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin"); const path = require("path"); module.exports = { plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "src", "index.html"), }), new CopyWebpackPlugin({ patterns: [ { from: "src/config", to: 'config' } ] }), ], };

И добавим команду build и необходимые зависимости от webpack в package.json:

{
  "name": "30-assets",
  "version": "1.0.0",
  "description": "Hello world app on NodeJs Express!",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "rm -rf dist && webpack --config webpack.config.js --mode development"
  },
  "author": "Flant",
  "license": "ISC",
  "dependencies": {
  },
  "devDependencies": {
    "copy-webpack-plugin": "^6.0.3",
    "css-loader": "^4.2.0",
    "file-loader": "^6.0.0",
    "html-webpack-plugin": "^4.3.0",
    "sass": "^1.26.10",
    "sass-loader": "^9.0.3",
    "style-loader": "^1.2.1",
    "webpack": "^4.44.1",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.0"
  }
}
{ "name": "30-assets", "version": "1.0.0", "description": "Hello world app on NodeJs Express!", "main": "app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "rm -rf dist && webpack --config webpack.config.js --mode development" }, "author": "Flant", "license": "ISC", "dependencies": { }, "devDependencies": { "copy-webpack-plugin": "^6.0.3", "css-loader": "^4.2.0", "file-loader": "^6.0.0", "html-webpack-plugin": "^4.3.0", "sass": "^1.26.10", "sass-loader": "^9.0.3", "style-loader": "^1.2.1", "webpack": "^4.44.1", "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.0" } }

Изменения в сборке

Для ассетов мы соберём отдельный образ с nginx и ассетами.

Создадим Dockerfile, осуществляющий сборку фронтенда:

FROM node:14-stretch as asset-builder
WORKDIR /app
COPY frontend/ .
RUN apt update
RUN apt install -y build-essential tzdata locales
RUN cd /app
RUN npm i
RUN ls -la
RUN cat package.json
RUN npm run build

###################################

FROM nginx:stable-alpine
COPY .werf/nginx.conf /etc/nginx/nginx.conf
COPY --from=asset-builder /app/dist /www
FROM node:14-stretch as asset-builder WORKDIR /app COPY frontend/ . RUN apt update RUN apt install -y build-essential tzdata locales RUN cd /app RUN npm i RUN ls -la RUN cat package.json RUN npm run build ################################### FROM nginx:stable-alpine COPY .werf/nginx.conf /etc/nginx/nginx.conf COPY --from=asset-builder /app/dist /www

Исходный код nginx.conf можно посмотреть в репозитории.

Скорректируем werf.yaml, чтобы он собирал не один, а два образа:

project: werf-guided-nodejs
configVersion: 1
---
image: basicapp
dockerfile: Dockerfile
---
image: node-assets
dockerfile: Dockerfile.frontend
project: werf-guided-nodejs configVersion: 1 --- image: basicapp dockerfile: Dockerfile --- image: node-assets dockerfile: Dockerfile.frontend

Изменения в инфраструктуре и роутинге

Инфраструктуру мы можем организовать двумя способами:

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

С помощью объекта Configmap мы будем пробрасывать в образ с nginx файл /config/env.json с актуальными именно для этого окружения данными.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: basicapp
spec:
  selector:
    matchLabels:
      app: basicapp
  revisionHistoryLimit: 3
  strategy:
    type: RollingUpdate
  replicas: 1
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
      labels:
        app: basicapp
    spec:
      imagePullSecrets:
      - name: "registrysecret"
      containers:
      - name: basicapp
        command: ["node","/app/app.js"]
        image: {{ .Values.werf.image.basicapp }}
        workingDir: /app
        ports:
        - containerPort: 3000
          protocol: TCP
        env:
        - name: "SQLITE_FILE"
          value: "app.db"
      - name: node-assets
        image: {{ index .Values.werf.image "node-assets" }}
        lifecycle:
          preStop:
            exec:
              command: ["/usr/sbin/nginx", "-s", "quit"]
        livenessProbe:
          httpGet:
            path: /healthz
            port: 80
            scheme: HTTP
        readinessProbe:
          httpGet:
            path: /healthz
            port: 80
            scheme: HTTP
        ports:
        - containerPort: 80
          name: http
          protocol: TCP
        volumeMounts:
        - name: env-json
          mountPath: /www/config/env.json
          subPath: env.json
      volumes:
      - name: env-json
        configMap:
          name: basicapp-configmap
apiVersion: apps/v1 kind: Deployment metadata: name: basicapp spec: selector: matchLabels: app: basicapp revisionHistoryLimit: 3 strategy: type: RollingUpdate replicas: 1 template: metadata: annotations: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} labels: app: basicapp spec: imagePullSecrets: - name: "registrysecret" containers: - name: basicapp command: ["node","/app/app.js"] image: {{ .Values.werf.image.basicapp }} workingDir: /app ports: - containerPort: 3000 protocol: TCP env: - name: "SQLITE_FILE" value: "app.db" - name: node-assets image: {{ index .Values.werf.image "node-assets" }} lifecycle: preStop: exec: command: ["/usr/sbin/nginx", "-s", "quit"] livenessProbe: httpGet: path: /healthz port: 80 scheme: HTTP readinessProbe: httpGet: path: /healthz port: 80 scheme: HTTP ports: - containerPort: 80 name: http protocol: TCP volumeMounts: - name: env-json mountPath: /www/config/env.json subPath: env.json volumes: - name: env-json configMap: name: basicapp-configmap

В сам Configmap нужное значение мы будем пробрасывать через глобальный аттрибут:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: basicapp-configmap
data:
  env.json: |
    {
      "url": {{ .Values.global.domain_url | quote }}
    }
--- apiVersion: v1 kind: ConfigMap metadata: name: basicapp-configmap data: env.json: | { "url": {{ .Values.global.domain_url | quote }} }

А у сервиса укажем оба порта:

apiVersion: v1
kind: Service
metadata:
  name: basicapp
spec:
  selector:
    app: basicapp
  ports:
  - name: http
    port: 3000
    protocol: TCP
  - name: http-nginx
    port: 80
    protocol: TCP
apiVersion: v1 kind: Service metadata: name: basicapp spec: selector: app: basicapp ports: - name: http port: 3000 protocol: TCP - name: http-nginx port: 80 protocol: TCP

Деплой

Закоммитим изменения в git и воспользуемся командой converge для сборки и деплоя, примерно так:

werf converge --repo localhost:5005/werf-guided-nodejs --set="global.domain_url=http://myverycustomdomain.io"

Обратите внимание, что мы пробрасываем кастомную настройку (домен) для фронтенда. Мы воспользовались одним из приёмов конфигурирования шаблона, упоминавшегося в главе “Конфигурирование инфраструктуры в виде кода” — в зависимости от ситуации вы можете аналогично воспользоваться методом с values.yaml или же подстановкой секретных переменных.

В реальной жизни настройки встраиваются в CI-процесс, что будет рассмотрено в главе “Работа с инфраструктурой”.