Константин Нежберт
Константин Нежберт
0
Обложка: Собираем и деплоим в Kubernetes приложение на Node.js с помощью werf

Собираем и деплоим в Kubernetes приложение на Node.js с помощью werf

В статье будет рассмотрено, как собрать Docker-контейнер Node.js-приложения и затем развернуть его в Kubernetes-кластере. Также рассмотрим, как можно легко накатывать изменения в коде и инфраструктуре, а также правильную организацию раздачи asset’ов, подняв для этого перед приложением reverse proxy-сервер.

В качестве замены K8s-кластеру воспользуемся minikube, это позволит малыми затратами подготовить локальное окружения для работы с werf.

О werf

werf — утилита командной строки, помогающая в организации всего цикла доставки ваших приложений в Kubernetes. В основе ее работы лежит едины источник истины — Git-репозиторий, в котором хранится само приложение и его конфигурация. Состояние приложения определяется коммитом, в котором оно зафиксировано. werf собирает его, заливает в container registry, при необходимости дособирая несуществующие слои конечных образов, после чего разворачивает приложение в кластере, обновляя его составный части, если они изменились. Дополнительно werf умеет удалять из container registry устаревшие артефакты. Для этого применяется уникальный алгоритм, основанный на истории Git-репозитория и политиках, настроенных пользователем.

Основная фишка werf — объединение различных инструментов, используемых DevOps-/SRE-инженерами и разработчиками в свой работе, в рамках одной утилиты. Это Git, Docker, container registry, CI-система, Helm и Kubernetes. Компоненты тесно интегрированы между собой и опираются на большой опыт разработчиков утилиты в «кубернетизации» приложений.

Предварительная настройка окружения

Для начала необходимо поставить актуальную версию werf из стабильной ветки. Инструкция по установке доступна в официальной документации.

Описанные далее действия тестировались на ОС Linux (Ubuntu 20.04.03). Утилита доступна и для других ОС (Windows или macOS), ее команды идентичны, но в случае других операционных систем могут быть небольшие особенности.

Установка зависимостей

Необходимо настроить работоспособный Kubernetes-кластер. Выполним следующие шаги:

  • настроим minikube — небольшой дистрибутив K8s, предназначенный для локальной установки;
  • установим в него NGINX Ingress Controller — специальный компонент, отвечающий за проброс запросов извне внутрь;
  • внесем изменения в /etc/hosts, чтобы обращаться к приложению в кластер по понятному имени имени, а не просто IP-адресу;
  • войдем под своей учетной записью на Docker Hub;
  • подготовим Secret с данными для входа в учетную запись Docker Hub;
  • развернем приложение в настроенном K8s-кластере.

Установка и настройка minikube

Поставьте minikube по инструкции из официальной документации. В случае, если вы его уже когда-то устанавливали, проверьте, что стоит актуальная версия.

Создайте K8s-кластер:

# Удаляем существующий minikube-кластер, если он существует
minikube delete
# Запускаем новый minikube-кластер
minikube start --driver=docker

При каждом запуске kubectl придется явно указывать пространство имен. Это неудобно, поэтому настроим его по умолчанию (обратите внимание, что сейчас мы только настроим имя, а само пространство имен не создается, это мы сделаем далее по тексту):

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

Если у вас нет kubectl, поставить ее можно одним из этих двух способов:

alias kubectl="minikube kubectl --"
echo 'alias kubectl="minikube kubectl --"' >> ~/.bash_aliases

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

Проверим состояние кластера, взглянув на запущенные в нем Pod’ы:

kubectl get --all-namespaces pod

Мы должны увидеть такую картину:

установка kubectl

Нужно обратить внимание на содержание столбцов READY и STATUS: все Pod’ы должны находиться в статусе Running, а их количество быть равным 1/1 (важно, чтобы левое число соответствовало правому). Если видите что-то другое, то нужно немного подождать — Pod’ы запускаются не мгновенно, поэтому до изменения их статуса на активный может пройти некоторое время.

Установка NGINX Ingress Controller

Далее установим Ingress-контроллер. Он нужен для того, чтобы запросы извне кластера пробрасывались внутрь на наше приложение.

Сделать это можно следующей командой:

minikube addons enable ingress

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

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

The 'ingress' addon is enabled

Подождем, пока он запустится, и проверим состояние кластера:

kubectl -n ingress-nginx get pod

Мы должны увидеть что-то наподобие этого:

Установка NGINX Ingress Controller

Если статус Pod’а в нижней строке должен быть Running.

Правим /etc/hosts

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

Добавим в файл адрес werf-first-app.test. Посмотрим IP-адрес кластера, выполнив команду minikube ip. Она должна отобразить правильный IP-адрес (например, 192.168.49.2). Если вы видите что-то другое, нужно пройти предыдущие шаги заново.

Для добавления строки в hosts запустим команду:

echo "$(minikube ip) werf-first-app.test" | sudo tee -a /etc/hosts

Чтобы проверить, что все сработало как надо, проверьте содержимое /etc/hosts любым доступным способом, в конце должна добавиться строка 192.168.49.2 werf-first-app.test.

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

curl http://werf-first-app.test

Т.к. приложение пока не запущено, Ingress ответит ошибкой 404:

<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>

Логинимся на Docker Hub

Собираемые образы нужно где-то хранить. Очевидное решение — приватный репозиторий на Docker Hub с именем werf-first-app.

Теперь нужно авторизоваться на Docker Hub под своим аккаунтом:

docker login
Username: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>
Password: <ПАРОЛЬ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>

В случае успешного входа отобразится сообщение Login Succeeded.

Генерация Secret для доступа к registry

Чтобы у werf был доступ к созданному container registry, необходимо подготовить Secret с параметрами для входа. Располагаться он должен в одном пространстве имен с приложением.

Создадим пространство имен приложения:

kubectl create namespace werf-first-app

В случае успешного выполнения мы увидим сообщение namespace/werf-first-app created.

Теперь сгенерируем Secret, задав ему имя registrysecret:

kubectl create secret docker-registry registrysecret 
  --docker-server='https://index.docker.io/v1/' 
  --docker-username='<ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>' 
  --docker-password='<ПАРОЛЬ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>'

Если отобразилось secret/registrysecret created, значит Secret создан. В случае возникновения ошибки при создании Secret удалите его kubectl delete secret registrysecret, а затем создайте еще раз. К сожалению, возможности отредактировать созданный Secret нет.

В документации Kubernetes этот метод создания Secret’ов указан как стандартный.

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

Базовое приложение на Node.js

Новое приложение на Node.js

Создадим новый каталог для приложения:

mkdir app

Перейдем в него и сгенерируем скелет приложения командой:

npx express-generator --git --no-view app

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

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

Добавим в файл app.js приложения новый endpoint /ping:

...
const indexRouter = require('./routes/index');
const pingRouter = require('./routes/ping');
...
app.use('/', indexRouter);
app.use('/ping', pingRouter);

Добавим новый контроллер, в котором будем обрабатывать запрос на созданный endpoint и возвращать ответ Hello, werfer!. Создадим файл routes/ping.js со следующим содержимым:

const express = require('express');
const router = express.Router();

router.get('/', function (req, res, next) {
  res.send('Hello, werfer!n');
});

module.exports = router;

Следующим шагом создадим Dockerfile, расположив его в корневом каталоге проекта и адаптировав под сборку приложения на Node.js:

FROM node:12-alpine
WORKDIR /app

# Копируем в образ файлы, нужные для установки зависимостей приложения.
COPY package.json package-lock.json ./
# Устанавливаем зависимости приложения.
RUN npm ci

# Копируем в образ все остальные файлы приложения.
COPY . .

Теперь необходимо создать манифесты для Kubernetes, описывающие ресурсы, которые будут в нем разворачиваться. Это будут привычные Helm-чарты.

Нам нужно не так много — Deployment, в котором будет разворачиваться само приложение, а также Service и Ingress, обеспечивающие доступ к приложению изнутри (между сервисами) и снаружи кластера.

Создадим каталог .helm в корне проекта со следующим содержимым:

.
└── templates
    ├── deployment.yaml
    ├── ingress.yaml
    └── service.yaml

Теперь заполним манифесты. Deployment приложения:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: werf-first-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: werf-first-app
  template:
    metadata:
      labels:
        app: werf-first-app
    spec:
      imagePullSecrets:
      - name: registrysecret
      containers:
      - name: app
        image: {{ .Values.werf.image.app }}
        command: ["npm", "start"]
        ports:
        - containerPort: 3000
        env:
        - name: DEBUG
          value: "app:*"

Ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
  name: werf-first-app
spec:
  rules:
  - host: werf-first-app.test
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: werf-first-app
            port:
              number: 3000

Service:

apiVersion: v1
kind: Service
metadata:
  name: werf-first-app
spec:
  selector:
    app: werf-first-app
  ports:
  - name: http
    port: 3000

Теперь остается последний шаг — добавить файл конфигурации werf. Это файл werf.yaml, расположенный в корне проекта и содержащий следующее содержимое:

project: werf-first-app
configVersion: 1

---
image: app
dockerfile: Dockerfile

Приложение готово к деплою!

Деплой приложения и проверка работоспособности

werf использует Git в качестве источника истины о том, с чем работает. Поэтому исходные файлы проекта должны находиться в Git-репозитории, пусть и просто локальном. Инициализируем новый репозиторий и зафиксируем изменения:

git init
git add .
git commit -m FIRST

Теперь развернем приложение в Kubernetes, выполнив команду:

werf converge --repo <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-first-app

Ожидаемый результат:

┌ ⛵ image app
│ Use cache image for app/dockerfile
│      name: <DOCKER HUB USERNAME>/werf-first-app:60e9598eb3606f800d8a8ce25e0573021df98da8d833e298ecf9ee26-1645189974890
│        id: d8b8a13f1d07
│   created: 2022-02-18 16:12:54 +0300 MSK
│      size: 29.8 MiB
└ ⛵ image app (1.62 seconds)

┌ Waiting for release resources to become ready
│ ┌ Status progress
│ │ DEPLOYMENT                                                                                                  REPLICAS            AVAILABLE             UP-TO-DATE
│ │ werf-first-app                                                                                              1/1                 1                     1
│ └ Status progress
└ Waiting for release resources to become ready (0.03 seconds)

Release "werf-first-app" has been upgraded. Happy Helming!
NAME: werf-first-app
LAST DEPLOYED: Fri Feb 18 16:19:23 2022
NAMESPACE: werf-first-app
STATUS: deployed
REVISION: 9
TEST SUITE: None
Running time 3.72 seconds

Проверим, что сервисы развернулись и функционируют:

curl http://werf-first-app.test/ping

В ответ должно отобразиться Hello, werfer!.

Раздаем assets правильно

Node.js-приложение способно самостоятельно заниматься раздачей статических файлов. Например, в express для этого можно создать контроллер на базе express.static. Однако разработчики express советуют использовать для раздачи статических файлов более эффективное решение, такое как проксирующий сервер NGINX. Эта рекомендация основана на том, что reverse proxy имеет лучшую производительность и эффективность раздачи статики, нежели штатные средства Node.js.

Сделать это можно несколькими способами. Мы будем использовать достаточно распространенный хорошо масштабируемый способ:

  • NGINX-контейнер поднимается перед каждым контейнером с Node.js в том же Pod’е;
  • поднятый контейнер будет проксировать все запросы, кроме запросов на статические файлы;
  • статические файлы хранятся в самом контейнере с NGINX и раздаются средствами последнего.

Новый endpoint /image

Теперь создадим в приложении новую страницу /image, на которой будут использоваться статические файлы. Собирать JS-, CSS- и медиафайлы будем с помощью webpack.

Располагаться статика будет в каталоге dist вместе с шаблоном главной страницы и страницы с изображением.

Добавим новый контроллер в routes/image.js:

...
/* GET home page. */
router.get('/', function (req, res, next) {
  res.sendFile(path.resolve(process.cwd(), 'dist', 'index.html'));
});

А также новый маршрут в routes/image.js:

const express = require('express');
const router = express.Router();

/* Image page. */
router.get('/', express.static('dist/image.html'));

module.exports = router;

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

Обновление сборки и деплоя

Организация файлов

Сейчас у нас есть только одна главная страница, доступная по адресу /. Ее содержимое (шаблон) лежит в каталоге /public/index.html. Добавим к ней еще одну — image.html, а затем реорганизуем структуру файлов в каталоге /public таким образом, чтобы разделить их на две части — pages и assets:

$ tree public
public/
├── assets
│   ├── images
│   │   └── werf-logo.svg
│   ├── javascripts
│   │   ├── image.js
│   │   └── index.js
│   └── stylesheets
│       ├── image.css
│       └── style.css
└── pages
    ├── image.html
    └── index.html

Страницы, основанные на шаблонах image и index, не будут кешироваться, т.к. их содержимое может быть динамическим. Однако связанные с страницами файлы мы будем собирать так, чтобы их имена менялись в зависимости от содержимого. Это позволит надолго кешировать файлы в браузерах пользователей и безболезненно изменять их в в случае необходимости. Содержимое каталога dist после сборки статики:

$ tree dist/
├── css
│   ├── image.40428375b4e566574c8f.css
│   └── index.1e6f9f5ee05a92734053.css
├── image.html
├── index.html
├── js
│   ├── image.e67ef581c6705e6bd9a0.js
│   ├── index.e5a57023092221b727e0.js
│   └── runtime.f9a303951d184e8c1ce3.js
└── media
    └── 2c6aa8e8ef0b96213f30.svg

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

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

$ npm install --save-dev 
  webpack webpack-cli webpack-dev-middleware 
  html-webpack-plugin 
  css-loader mini-css-extract-plugin css-minimizer-webpack-plugin

Назначение добавленных модулей:

  • webpack, webpack-cli — непосредственно собирает статические файлы;
  • webpack-dev-middleware — раздает статические файлы во время разработки;
  • html-webpack-plugin — генерирует HTML-страницы с динамическими статическими файлами;
  • css-loader — задействовует CSS в сборке;
  • mini-css-extract-plugin — выделяет CSS в отдельные файлы, что позволяет кешировать их отдельно от JS;
  • css-minimizer-webpack-plugin — минифициует CSS (будет работать только при включении mode: "production").

Теперь добавим в package.json параметр "build": "webpack". В app.js добавим обработчик запросов к статическим файлам. Это необходимо на случай, если приложение будет запущено не в production-окружении:

...
const app = express();

if (process.env.NODE_ENV != 'production') {
  const webpack = require('webpack');
  const webpackDevMiddleware = require('webpack-dev-middleware');
  const config = require('./webpack.config.js');

  const compiler = webpack(config);

  app.use(
    webpackDevMiddleware(compiler, {
      publicPath: config.output.publicPath,
    })
  );
}

Сконфигурируем webpack в файле webpack.config.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  mode: 'development',
  devServer: { static: '/dist' },
  devtool: 'inline-source-map',

  // Две точки входа, для которых мы собираем бандлы. Таким образом мы разделяем стили и
  // скрипты для двух страниц.
  entry: {
    index: path.resolve(__dirname, 'public/assets/javascripts/index.js'),
    image: path.resolve(__dirname, 'public/assets/javascripts/image.js'),
  },

  // Определяем, как называются переработанные скрипты и с каким префиксом ожидать
  // статические файлы.
  output: {
    filename: 'js/[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
    publicPath: '/static/',
  },

  plugins: [
    // Подключаем плагин для выделения CSS в отдельные файлы.
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash].css',
    }),
    // Генерируем главную страницу из ее шаблона и указываем, что подключить нужно только
    // точку входа index, включая CSS и JS. Так мы избежим подключения скриптов
    // и стилей от страницы с изображением.
    new HtmlWebpackPlugin({
      title: 'werf first app',
      template: 'public/pages/index.html',
      chunks: ['index'],
    }),
    // То же самое для страницы c изображением, но еще явно указываем конечное имя HTML-файла.
    new HtmlWebpackPlugin({
      title: 'werf logo',
      filename: 'image.html',
      template: 'public/pages/image.html',
      chunks: ['image'],
    }),
  ],
  module: {
    rules: [
      {
        test: /.css$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
      // Изображения перенесем в каталог /static/media, причем часть "static" мы определили
      // в настройках "output".
      {
        test: /.(png|svg|jpg|jpeg|gif)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'media/[contenthash][ext]',
        },
      },
    ],
  },
  // С помощью оптимизаций мы выделили скрипт с рантаймом, чтобы уменьшить общий размер статики,
  // который нужно будет загрузить для разных страниц. Также добавили минимизацию CSS для production.
  optimization: {
    moduleIds: 'deterministic',
    runtimeChunk: 'single',
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\/]node_modules[\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
    minimizer: [new CssMinimizerPlugin()],
  },
};

Убедимся, что все настроено правильно — запустим локально приложение и взглянем на результат, доступный на странице http://localhost:3000/image:

npm run build && npm start

Переходим к деплою в кластер.

Подготовка деплоя в кластер

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

Адаптируем Dockerfile:

FROM node:12-alpine as builder
WORKDIR /app

# Копируем в образ файлы, нужные для установки зависимостей приложения.
COPY package.json package-lock.json ./

# Устанавливаем зависимости приложения.
RUN npm ci

# Копируем в образ все остальные файлы приложения.
COPY . .

# Собираем статические файлы.
RUN npm run build

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

FROM node:12-alpine as backend
WORKDIR /app

# Копируем в образ файлы, нужные для установки зависимостей приложения.
COPY package.json package-lock.json ./

# Устанавливаем зависимости приложения.
RUN npm ci --production

# Копируем файлы приложения.
COPY app.js ./
RUN mkdir dist
COPY --from=builder /app/dist/*.html ./dist/
COPY --from=builder /app/bin         ./bin/
COPY --from=builder /app/routes      ./routes/


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

# NGINX-образ с собранными ранее ассетами.
FROM nginx:stable-alpine as frontend
WORKDIR /www

# Копируем собранные ассеты из предыдушего сборочного образа.
COPY --from=builder /app/dist /www/static

# Копируем конфигурацию NGINX.
COPY .werf/nginx.conf /etc/nginx/nginx.conf

На последнем шаге происходит копирование файла конфигурации NGINX в контейнер. Создадим его по пути .werf/nginx.conf:

user nginx;
worker_processes 1;
pid /run/nginx.pid;

events {
  worker_connections 1024;
}

http {
  include /etc/nginx/mime.types;
  default_type application/octet-stream;

  upstream backend {
    server 127.0.0.1:3000 fail_timeout=0;
  }

  server {
    listen 80;
    server_name _;

    root /www;

    client_max_body_size 100M;
    keepalive_timeout 10s;

    # По пути /static отдадим ассеты напрямую из файловой системы NGINX-контейнера.
    location /static {
      # В силу особенностей механизма сборки ассетов с Webpack клиент может хранить кэш ассетов сколь
      # угодно долго, не беспокоясь об их инвалидации.
      expires 1y;
      add_header Cache-Control public;
      add_header Last-Modified "";
      add_header ETag "";

      # Когда есть возможность, отдаём заранее сжатые файлы (вместо сжатия на лету).
      gzip_static on;

      access_log off;

      try_files $uri =404;
    }

    # Ассеты медиафайлов (картинки и т.п.) также отдадим из файловой системы NGINX-контейнера, но
    # отключим для них сжатие gzip.
    location /static/media {
      expires 1y;
      add_header Cache-Control public;
      add_header Last-Modified "";
      add_header ETag "";

      access_log off;

      try_files $uri =404;
    }

    # Все запросы, кроме запросов на получение ассетов, отправляются на Node.js-бэкенд.
    location / {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Host $http_host;
      proxy_redirect off;

      proxy_pass http://backend;
    }
  }
}

Сейчас werf не знает о том, что мы поделили приложение на составные части. Добавим новую конфигурацию в файле werf.yaml:

project: werf-first-app
configVersion: 1

---
image: backend
dockerfile: Dockerfile
target: backend

---
image: frontend
dockerfile: Dockerfile
target: frontend

Осталось добавить контейнер с proxy в Deployment приложения:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: werf-first-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: werf-first-app
  template:
    metadata:
      labels:
        app: werf-first-app
    spec:
      imagePullSecrets:
      - name: registrysecret
      containers:
      - name: backend
        image: {{ .Values.werf.image.backend }}
        command: ["node", "./bin/www"]
        ports:
        - containerPort: 3000
        env:
        - name: NODE_ENV
          value: production
      - name: frontend
        image: {{ .Values.werf.image.frontend }}
        ports:
        - containerPort: 80

И настроить связи между контейнерами и внешним миром, изменив Ingress и Sevice:

apiVersion: v1
kind: Service
metadata:
  name: werf-first-app
spec:
  selector:
    app: werf-first-app
  ports:
  - name: http
    port: 80
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
  name: werf-first-app
spec:
  rules:
  - host: werf-first-app.test
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: werf-first-app
            port:
              number: 80

Приложение готово к работе и спрятано за reverse proxy.

Проверка работоспособности

Редеплоим приложение в Kubernetes, выполнив команду (не забудьте перед этим закоммитить изменения в репозитории):

werf converge --repo <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-first-app

Ожидаемый результат:

┌ Concurrent builds plan (no more than 5 images at the same time)
│ Set #0:
│ - ⛵ image backend
│ - ⛵ image frontend
└ Concurrent builds plan (no more than 5 images at the same time)

┌ ⛵ image backend
│ Use cache image for backend/dockerfile
│      name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-first-app:9b057cc5b3cbe8ffe8aa116167625932a553fd5f516f01b50642d8bd-1645443842370
│        id: 8f33ddcc2a8f
│   created: 2022-02-21 14:44:02 +0300 MSK
│      size: 30.7 MiB
└ ⛵ image backend (1.59 seconds)

┌ ⛵ image frontend
│ Use cache image for frontend/dockerfile
│      name: <ИМЯ ПОЛЬЗОВАТЕЛЯ DOCKER HUB>/werf-first-app:f19f3e564d3f306b7469717a2c939090ef81b21c4efa68f108841ddd-1645443844850
│        id: adb90b8d90ab
│   created: 2022-02-21 14:44:04 +0300 MSK
│      size: 9.6 MiB
└ ⛵ image frontend (1.59 seconds)

┌ Waiting for release resources to become ready
│ ┌ Status progress
│ │ DEPLOYMENT                                                                                                                                                REPLICAS                     AVAILABLE                      UP-TO-DATE
│ │ werf-first-app                                                                                                                                            1/1                          1                              1                                              ↵
│ │
│ └ Status progress
└ Waiting for release resources to become ready (0.03 seconds)

Release "werf-first-app" has been upgraded. Happy Helming!
NAME: werf-first-app
LAST DEPLOYED: Mon Feb 21 14:48:27 2022
NAMESPACE: werf-first-app
STATUS: deployed
REVISION: 11
TEST SUITE: None
Running time 3.96 seconds

Проверим работоспособность: обратимся к странице http://werf-first-app.test/image и нажмем на ней кнопку «Get Image». Мы должны увидеть следующее:

Теперь посмотрим, какие запросы были выполнены и по каким адресам:

редеплой Kubernetes

Мы превратили наше API в полноценное web-приложение, имеющее средства для эффективного управления JS- и статическими файлами. Также оно способно выдержать высокую нагрузку при большом количестве запросов к статическим файлам, и это не будет сказываться на работоспособности самого приложения.

Также можно быстро масштабировать приложение и proxy перед ним, просто увеличивая количество реплик в Deployment’е приложения.

Заключение

Мы научились собирать и деплоить в k8S-кластер простое приложение на Node.js с помощью утилиты werf. При этом мы правильно организовали раздачу ассетов, спрятав бэкенд за reverse proxy-сервером NGINX, сняв тем самым нагрузку с самого приложения.

Надеюсь, что этот материал поможет вам научиться эффективно использовать werf и Node.js, а также получить немного опыта в деплое приложений в Kubernetes!

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