Добавить в корзинуПозвонить
Найти в Дзене
Николай Калюжный

Установка и настройка высокодоступного кластера Kubernetes v1.29 и v1.31 на Rocky Linux 9.5 с CRI-O в качестве среды выполнения контейнера

В этой статье блога мы будем реализовывать кластеры Kubernetes с высокой доступностью (HA) с помощью kubeadm! В этом руководстве мы рассмотрим, как использовать kubeadm в Rocky Linux 9.4 и 9.5 (проверено на обоих дистрибутивах) для настройки устойчивых и отказоустойчивых сред Kubernetes. Независимо от того, являетесь ли вы новичком в системах высокой доступности или хотите повысить надежность существующего кластера, в этой статье вы найдете практические идеи и пошаговые инструкции, которые помогут вам обеспечить бесперебойную работу и масштабируемость. Мы будем использовать последнюю версию Kubernetes v1.31.5 При использовании многоуровневых узлов плоскости управления этот подход сводит к минимуму требования к инфраструктуре за счет совместного размещения членов etcd и узлов плоскости управления. 3 узла, требования к плоскости управления — 4 ГБ, 4 ЦП, 50 ГБ
3 узла, требования к рабочему узлу — 16 ГБ, 8 ЦП, 100 ГБ
1 узел, требования к спецификации узла HAProxy — 2 ГБ, 2 ЦП, 50 ГБ DNS-за
Оглавление

В этой статье блога мы будем реализовывать кластеры Kubernetes с высокой доступностью (HA) с помощью kubeadm! В этом руководстве мы рассмотрим, как использовать kubeadm в Rocky Linux 9.4 и 9.5 (проверено на обоих дистрибутивах) для настройки устойчивых и отказоустойчивых сред Kubernetes. Независимо от того, являетесь ли вы новичком в системах высокой доступности или хотите повысить надежность существующего кластера, в этой статье вы найдете практические идеи и пошаговые инструкции, которые помогут вам обеспечить бесперебойную работу и масштабируемость.

Мы будем использовать последнюю версию Kubernetes v1.31.5

При использовании многоуровневых узлов плоскости управления этот подход сводит к минимуму требования к инфраструктуре за счет совместного размещения членов etcd и узлов плоскости управления.

-2

Требования к системе

3 узла, требования к плоскости управления — 4 ГБ, 4 ЦП, 50 ГБ
3 узла, требования к рабочему узлу — 16 ГБ, 8 ЦП, 100 ГБ
1 узел, требования к спецификации узла HAProxy — 2 ГБ, 2 ЦП, 50 ГБ

DNS-записи для всех узлов кластера k8s и HAProxy создаются в IDM.

Полное доменное имяIP-адресkube-apiserver.kalyuzhnyy.ru192.168.0.50k8smas1.kalyuzhnyy.ru192.168.0.51k8smas2.kalyuzhnyy.ru192.168.0.52k8smas3.kalyuzhnyy.ru192.168.0.53k8swor1.kalyuzhnyy.ru192.168.0.54k8swor2.kalyuzhnyy.ru192.168.0.55k8swor3.kalyuzhnyy.ru192.168.0.56Полное доменное имя и IP-адрес для кластера Kubernetes HA

Настройка HAProxy

Если вы хотите настроить HAProxy для Kubernetes, обратитесь к

Инструменты системного администратора

Установите следующие предварительные требования на всех узлах

dnf install nano mc iputils wget curl vim bash-completion nc tcpdump telnet bind-utils -y

Шаг 1: Установите имя хоста и обновите файл хостов

Войдите в систему или ssh на каждой машине и выполните команды hostnamectl, чтобы задать соответствующее имя хоста.

sudo hostnamectl set-hostname “kube-apiserver.kalyuzhnyy.ru” && exec bash
sudo hostnamectl set-hostname “k8smas1.kalyuzhnyy.ru” && exec bash
sudo hostnamectl set-hostname “k8smas2.kalyuzhnyy.ru” && exec bash
sudo hostnamectl set-hostname “k8smas3.kalyuzhnyy.ru” && exec bash
sudo hostnamectl set-hostname “k8swor1.kalyuzhnyy.ru” && exec bash
sudo hostnamectl set-hostname “k8swor2.kalyuzhnyy.ru” && exec bash
sudo hostnamectl set-hostname “k8swor3.kalyuzhnyy.ru” && exec bash
sudo hostnamectl set-hostname “k8swor4.kalyuzhnyy.ru” && exec bash
sudo hostnamectl set-hostname “k8swor5.kalyuzhnyy.ru” && exec bash

и т.д. для соответствующего сервера своя запись sudo hostnamectl set-hostname...

Добавьте следующие записи в файл

nano /etc/hosts на каждом узле.

192.168.0.50 kube-apiserver.kalyuzhnyy.ru
192.168.0.51 k8smas1.kalyuzhnyy.ru
192.168.0.52 k8smas2.kalyuzhnyy.ru
192.168.0.53 k8smas3.kalyuzhnyy.ru
192.168.0.54 k8swor1.kalyuzhnyy.ru
192.168.0.55 k8swor2.kalyuzhnyy.ru
192.168.0.56 k8swor3.kalyuzhnyy.ru

Шаг 2: Отключите пространство подкачки на каждом узле

Удалите записи подкачки Swap из /etc/default/grub

sudo swapoff -a
sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab

Сохраните и выйдите, а затем введите команду

grub2-mkconfig -o /boot/grub2/grub.cfg

Шаг 3: Настройте правила SELinux и Firewall для Kubernetes

Установите режим SELinux как разрешающий на всех узлах с помощью следующих команд:

sudo setenforce 0
sudo sed -i --follow-symlinks 's/SELINUX=enforcing/SELINUX=permissive/g' /etc/sysconfig/selinux

Требования к межсетевому экрану

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

На главном узле разрешите следующие порты в брандмауэре.

На плоскости управления или главных узлах

firewall-cmd --permanent --add-service={kube-apiserver,kube-control-plane,kube-control-plane-secure,kube-api,kube-worker}
firewall-cmd --permanent --zone=trusted --add-interface=lo
firewall-cmd --permanent --zone=trusted --add-service={kube-api,kube-scheduler,kube-scheduler-secure,kube-controller-manager}
firewall-cmd --reload
firewall-cmd --list-all-zones

или

sudo firewall-cmd --zone=public --permanent --add-port=6443/tcp
sudo firewall-cmd --zone=public --permanent --add-port=2379-2380/tcp
sudo firewall-cmd --zone=public --permanent --add-port=10250/tcp
sudo firewall-cmd --zone=public --permanent --add-port=10251/tcp
sudo firewall-cmd --zone=public --permanent --add-port=10252/tcp
sudo firewall-cmd --zone=public --permanent --add-port=10255/tcp
sudo firewall-cmd --zone=public --permanent --add-port=5473/tcp
sudo firewall-cmd --zone=public --permanent --add-port=8080/tcp

или
sudo firewall-cmd --permanent --add-port={6443,2379,2380,10250,10251,10252,10257,10259,179}/tcp
sudo firewall-cmd --permanent --add-port=4789/udp
sudo firewall-cmd --reload

Или выключить фаервол
systemctl stop firewalld
systemctl disable firewalld

На рабочих узлах

firewall-cmd --permanent --add-service=kube-worker
firewall-cmd --permanent --zone=trusted --add-interface=lo
firewall-cmd --permanent --zone=trusted --add-service=kube-api
firewall-cmd --reload
firewall-cmd --list-all-zones

Шаг 4: Добавьте модули и параметры ядра

Переадресация IPv4 и предоставление iptables возможности видеть трафик по мостам

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

Параметры sysctl, необходимые во время настройки.

cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF

Включение модулей и проверка.

modprobe overlay
modprobe br_netfilter
lsmod | grep br_netfilter
lsmod | grep overlay

Применяйте параметры без перезагрузки серверов

sysctl --system

Убедитесь, что установлено значение 1.

sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forward

Установка контейнерных сред выполнения CRI-O

В этой конфигурации мы предпочитаем использовать CRI-O в качестве среды выполнения контейнера, хотя доступны и другие варианты, такие как containerd, Docker и Mirantis.

Установите переменную для версии платформы Kubernetes.

Для установки версии 1.29 введите

KUBERNETES_VERSION=v1.29

Для установки версии 1.31 введите

KUBERNETES_VERSION=v1.31
PROJECT_PATH=prerelease:/main

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

Создание необходимых репозиториев Kubernetes и CRI-O

cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/$KUBERNETES_VERSION/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/$KUBERNETES_VERSION/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF

cat <<EOF | tee /etc/yum.repos.d/cri-o.repo
[cri-o]
name=CRI-O
baseurl=https://pkgs.k8s.io/addons:/cri-o:/$PROJECT_PATH/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/addons:/cri-o:/$PROJECT_PATH/rpm/repodata/repomd.xml.key
EOF
Установите, запустите и включите службу.

dnf install -y container-selinux
dnf install -y cri-o
systemctl start crio.service
systemctl enable crio.service

Установка пакетов Kubernetes

И финальные пакеты, необходимые для настройки кластера kubeadm HA

dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
systemctl enable --now kubelet

Инициализация кластера

Для начала нам нужно инициализировать на любой из плоскостей управления

kubeadm init --control-plane-endpoint "kube-apiserver.kalyuzhnyy.ru:6443" --upload-certs

[root@kube-apiserver nkalyuzhnyy]# kubeadm init --control-plane-endpoint "kube-apiserver.kalyuzhnyy.ru:6443" --upload-certs

[addons] Applied essential addon: kube-proxy

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

Alternatively, if you are the root user, you can run:

export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
https://kubernetes.io/docs/concepts/cluster-administration/addons/

You can now join any number of the control-plane node running the following command on each as root:

kubeadm join kube-apiserver.kalyuzhnyy.ru:6443 --token eifbd3.2wkmh307w5pmkmmo \
--discovery-token-ca-cert-hash sha256:ffebcd332141b6b91b4e81b6a2a0c71a6bf17fc3cc5d16a4e3280534121f4cf0 \
--control-plane --certificate-key cb6598d98bfcc1139ff7f006e0c16ac716bc9855366610d1abe661d4fc220dff

Please note that the certificate-key gives access to cluster sensitive data, keep it secret!
As a safeguard, uploaded-certs will be deleted in two hours; If necessary, you can use
"kubeadm init phase upload-certs --upload-certs" to reload certs afterward.

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join kube-apiserver.kalyuzhnyy.ru:6443 --token eifbd3.2wkmh307w5pmkmmo \
--discovery-token-ca-cert-hash sha256:ffebcd332141b6b91b4e81b6a2a0c71a6bf17fc3cc5d16a4e3280534121f4cf0


[root@kube-apiserver nkalyuzhnyy]#
[root@kube-apiserver nkalyuzhnyy]#

Как только мы выполним приведенную выше команду, она успеет загрузить необходимые образы для настройки кластера kubernetes.

Для версии Kubernetes 1.29 вывод следующий

[root@kube-apiserver nkalyuzhnyy]# crictl images
IMAGE TAG IMAGE ID SIZE
registry.k8s.io/coredns/coredns v1.11.1 cbb01a7bd410d 61.2MB
registry.k8s.io/etcd 3.5.16-0 a9e7e6b294baf 151MB
registry.k8s.io/kube-apiserver v1.29.13 724efdc6b8440 129MB
registry.k8s.io/kube-controller-manager v1.29.13 04dd549807d44 124MB
registry.k8s.io/kube-proxy v1.29.13 f20cf1600da6c 84.2MB
registry.k8s.io/kube-scheduler v1.29.13 42b8a40668702 61.3MB
registry.k8s.io/pause 3.10 873ed75102791 742kB
registry.k8s.io/pause 3.9 e6f1816883972 750kB
[root@kube-apiserver nkalyuzhnyy]#


[root@kube-apiserver nkalyuzhnyy]#
[root@kube-apiserver nkalyuzhnyy]#

Для версии Kubernetes 1.31.5 вывод

[root@kube-apiserver ~]# crictl images
IMAGE TAG IMAGE ID SIZE
docker.io/calico/cni v3.25.0 d70a5947d57e5 198MB
docker.io/calico/kube-controllers v3.25.0 5e785d005ccc1 71.7MB
docker.io/calico/node v3.25.0 08616d26b8e74 247MB
registry.k8s.io/coredns/coredns v1.11.3 c69fa2e9cbf5f 63.3MB
registry.k8s.io/etcd 3.5.15-0 2e96e5913fc06 149MB
registry.k8s.io/kube-apiserver v1.31.5 2212e74642e45 95.3MB
registry.k8s.io/kube-controller-manager v1.31.5 d7fccb640e0ed 89.5MB
registry.k8s.io/kube-proxy v1.31.5 34018aef09a62 92.8MB
registry.k8s.io/kube-scheduler v1.31.5 4b2fb209f5d1e 68.5MB
registry.k8s.io/pause 3.10 873ed75102791 742kB
[root@kube-apiserver ~]#
[root@kube-apiserver ~]# crictl images IMAGE TAG IMAGE ID SIZE docker.io/calico/cni v3.25.0 d70a5947d57e5 198MB docker.io/calico/kube-controllers v3.25.0 5e785d005ccc1 71.7MB docker.io/calico/node v3.25.0 08616d26b8e74 247MB registry.k8s.io/coredns/coredns v1.11.3 c69fa2e9cbf5f 63.3MB registry.k8s.io/etcd 3.5.15-0 2e96e5913fc06 149MB registry.k8s.io/kube-apiserver v1.31.5 2212e74642e45 95.3MB registry.k8s.io/kube-controller-manager v1.31.5 d7fccb640e0ed 89.5MB registry.k8s.io/kube-proxy v1.31.5 34018aef09a62 92.8MB registry.k8s.io/kube-scheduler v1.31.5 4b2fb209f5d1e 68.5MB registry.k8s.io/pause 3.10 873ed75102791 742kB [root@kube-apiserver ~]#

Подготовьте обычного пользователя к доступу к API.

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
export KUBECONFIG=/etc/kubernetes/admin.conf
kubectl cluster-info dump

Присоединение к другим хостам управления (Control Planes)

Как только мы получим вышеуказанный вывод, соединим другие плоскости управления с помощью команды

kubeadm join kube-apiserver.kalyuzhnyy.ru:6443 --token eifbd3.2wkmh307w5pmkmmo \
--discovery-token-ca-cert-hash sha256:ffebcd332141b6b91b4e81b6a2a0c71a6bf17fc3cc5d16a4e3280534121f4cf0 \
--control-plane --certificate-key cb6598d98bfcc1139ff7f006e0c16ac716bc9855366610d1abe661d4fc220dff

В случае возникновения проблем с подключением из-за сертификата, перегенируйте его

Обновление сертификата на api-kube.kalyuzhnyy.ru

kubeadm init phase upload-certs --upload-certs

[root@kube-apiserver nkalyuzhnyy]# kubeadm init phase upload-certs --upload-certs
I0206 13:11:54.728474 6711 version.go:256] remote version is much newer: v1.32.1; falling back to: stable-1.29
[upload-certs] Storing the certificates in Secret "kubeadm-certs" in the "kube-system" Namespace
[upload-certs] Using certificate key:
1da7f72675c5313b681ed4990393996c43a25d81423a28efb17a856cc1db5771
[root@kube-apiserver nkalyuzhnyy]# kubeadm init phase upload-certs --upload-certs I0206 13:11:54.728474 6711 version.go:256] remote version is much newer: v1.32.1; falling back to: stable-1.29 [upload-certs] Storing the certificates in Secret "kubeadm-certs" in the "kube-system" Namespace [upload-certs] Using certificate key: 1da7f72675c5313b681ed4990393996c43a25d81423a28efb17a856cc1db5771

Присоединение рабочих узлов

Сразу после плоскости управления начните присоединяться к рабочим узлам, выполнив команду

kubeadm join kube-apiserver.kalyuzhnyy.ru:6443 --token eifbd3.2wkmh307w5pmkmmo \
--discovery-token-ca-cert-hash sha256:ffebcd332141b6b91b4e81b6a2a0c71a6bf17fc3cc5d16a4e3280534121f4cf0

Проверка установленной версии кластера kubectl version для Kubernetes 1.29.13

-7
Проверка установленной версии кластера kubectl version для Kubernetes 1.31.5 kubeadm version
kubeadm version: &version.Info{Major:"1", Minor:"31", GitVersion:"v1.31.5", 
GitCommit:"af64d838aacd9173317b39cf273741816bd82377", GitTreeState:"clean", 
BuildDate:"2025-01-15T14:39:21Z", GoVersion:"go1.22.10", Compiler:"gc", Platform:"linux/amd64"}
kubeadm version
Client Version: v1.31.5
Kustomize Version: v5.4.2
Server Version: v1.31.5
Проверка установленной версии кластера kubectl version для Kubernetes 1.31.5 kubeadm version kubeadm version: &version.Info{Major:"1", Minor:"31", GitVersion:"v1.31.5", GitCommit:"af64d838aacd9173317b39cf273741816bd82377", GitTreeState:"clean", BuildDate:"2025-01-15T14:39:21Z", GoVersion:"go1.22.10", Compiler:"gc", Platform:"linux/amd64"} kubeadm version Client Version: v1.31.5 Kustomize Version: v5.4.2 Server Version: v1.31.5
kubectl get nodes -o wide
kubectl get nodes -o wide

Создание сети подов

Теперь мы должны развернуть сеть pod для кластера, я собираюсь использовать Calico.

Скачайте последний файл YAML с официального сайта calico и создайте сеть подов.

# wget https://docs.projectcalico.org/manifests/calico.yaml
# kubectl apply -f calico.yaml

Мы должны увидеть, как calico работает на каждом узле

kubectl get pods -n kube-system

[root@kube-apiserver ~]# kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
calico-kube-controllers-6879d4fcdc-82jj8 1/1 Running 0 103m
calico-node-7hrph 1/1 Running 0 103m
calico-node-lw888 1/1 Running 0 48m
calico-node-mq4zd 1/1 Running 0 103m
calico-node-mqsn7 1/1 Running 0 66m
coredns-7c65d6cfc9-lxmgc 1/1 Running 0 132m
coredns-7c65d6cfc9-v76c2 1/1 Running 0 132m
etcd-k8smas1.kalyuzhnyy.ru 1/1 Running 0 66m
etcd-kube-apiserver.kalyuzhnyy.ru 1/1 Running 2 133m
kube-apiserver-k8smas1.kalyuzhnyy.ru 1/1 Running 0 66m
kube-apiserver-kube-apiserver.kalyuzhnyy.ru 1/1 Running 44 133m
kube-controller-manager-k8smas1.kalyuzhnyy.ru 1/1 Running 0 66m
kube-controller-manager-kube-apiserver.kalyuzhnyy.ru 1/1 Running 2 133m
kube-proxy-6m782 1/1 Running 0 48m
kube-proxy-85x7n 1/1 Running 0 132m
kube-proxy-s568v 1/1 Running 0 110m
kube-proxy-vhngp 1/1 Running 0 66m
kube-scheduler-k8smas1.kalyuzhnyy.ru 1/1 Running 0 66m
kube-scheduler-kube-apiserver.kalyuzhnyy.ru 1/1 Running 2 133m
[root@kube-apiserver ~]#

Тестирование развертывания приложения

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

[root@kube-apiserver ~]$ kubectl create namespace webserver
namespace/webserver created
[root@kube-apiserver ~]

[root@kube-apiserver ~]$ kubectl get namespaces
NAME STATUS AGE
default Active 8h
kube-node-lease Active 8h
kube-public Active 8h
kube-system Active 8h
webserver Active 6s
[root@kube-apiserver ~]

[root@kube-apiserver ~]$ kubectl create deployment webserver --image nginx --replicas 3 --namespace webserver
deployment.apps/webserver created
[root@kube-apiserver ~]

[root@kube-apiserver ~]$ kubectl get deployments.apps -n webserver
NAME READY UP-TO-DATE AVAILABLE AGE
webserver 3/3 3 3 18s
[root@kube-apiserver ~]$

kubectl get pods -n webserver -o wide
kubectl get pods -n webserver -o wide

kubectl logs -n webserver deployments/webserver

[root@kube-apiserver ~]# kubectl logs -n webserver deployments/webserver
Found 3 pods, using pod/webserver-659c684488-25q6z
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2025/02/07 12:27:07 [notice] 1#1: using the "epoll" event method
2025/02/07 12:27:07 [notice] 1#1: nginx/1.27.4
2025/02/07 12:27:07 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14)
2025/02/07 12:27:07 [notice] 1#1: OS: Linux 5.14.0-427.13.1.el9_4.x86_64
2025/02/07 12:27:07 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2025/02/07 12:27:07 [notice] 1#1: start worker processes
2025/02/07 12:27:07 [notice] 1#1: start worker process 24
2025/02/07 12:27:07 [notice] 1#1: start worker process 25
2025/02/07 12:27:07 [notice] 1#1: start worker process 26
2025/02/07 12:27:07 [notice] 1#1: start worker process 27
[root@kube-apiserver ~]#

Вот и все, мы закончили с настройкой высокодоступного кластера Kubernetes на Rocky Linux 9.4 и 9.5 с CRI-O в качестве среды выполнения контейнера.

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

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

А также вы всегда можете поддержать меня зайдя на сайт и подписаться https://dzen.ru/kalyuzhnyy.ru и найти больше статей на моих ресурсах https://kalyuzhnyy.ru и https://dev.kalyuzhnyy.ru