В статье будет рассмотрено, как собрать 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
, поставить ее можно одним из этих двух способов:
- по инструкции из официальной документации;
- воспользоваться входящей в состав minikube версией утилиты, для чего выполните две команды:
alias kubectl="minikube kubectl --"
echo 'alias kubectl="minikube kubectl --"' >> ~/.bash_aliases
Во втором случае при первом запуске kubectl нужная версия утилиты загрузится из репозиториев minikube и станет доступна для использования.
Проверим состояние кластера, взглянув на запущенные в нем Pod’ы:
kubectl get --all-namespaces pod
Мы должны увидеть такую картину:
Нужно обратить внимание на содержание столбцов 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
Мы должны увидеть что-то наподобие этого:
Если статус 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». Мы должны увидеть следующее:
Теперь посмотрим, какие запросы были выполнены и по каким адресам:
Мы превратили наше API в полноценное web-приложение, имеющее средства для эффективного управления JS- и статическими файлами. Также оно способно выдержать высокую нагрузку при большом количестве запросов к статическим файлам, и это не будет сказываться на работоспособности самого приложения.
Также можно быстро масштабировать приложение и proxy перед ним, просто увеличивая количество реплик в Deployment’е приложения.
Заключение
Мы научились собирать и деплоить в k8S-кластер простое приложение на Node.js с помощью утилиты werf. При этом мы правильно организовали раздачу ассетов, спрятав бэкенд за reverse proxy-сервером NGINX, сняв тем самым нагрузку с самого приложения.
Надеюсь, что этот материал поможет вам научиться эффективно использовать werf и Node.js, а также получить немного опыта в деплое приложений в Kubernetes!
В следующей статье мы разберемся, как правильно организовать работу с базой данных, развернув ее в K8s и инициализировав со всеми миграциями и настройками.