Предыстория
Когда изучаешь DevOps по курсам — всё выглядит просто. Запустил minikube, поднял pod, посмотрел на kubectl get pods — красота. А потом пытаешься сделать что-то настоящее и понимаешь: между “hello world в Kubernetes” и реальной инфраструктурой — пропасть.
Я решил эту пропасть пройти. Взял 5 виртуальных машин на VMware Workstation и построил на них production-ready кластер с нуля. С CI/CD, GitOps, мониторингом, высокой доступностью и реальным приложением.
Расскажу что получилось, как именно это работает — и главное, какие грабли я собрал по дороге. Их было немало.
Окружение: Ubuntu 22.04 LTS (ubuntu-22.04.4-live-server-amd64), VMware Workstation 17, Kubernetes 1.29, Calico 3.29.3
Результат
React + Go + FastAPI + PostgreSQL, всё в Kubernetes
|
Компонент |
Версия / Детали |
|---|---|
|
Kubernetes |
1.29, kubeadm, 1 master + 4 workers |
|
CNI |
Calico v3.29.3, режим IPIP |
|
Load Balancer |
MetalLB v0.14.9, L2 ARP |
|
Ingress |
ingress-nginx v1.10 |
|
CI/CD |
GitHub Actions + ArgoCD v2.10 |
|
БД |
CloudNativePG v1.23, 1 primary + 2 replica |
|
Мониторинг |
kube-prometheus-stack chart v65.1.1 |
|
IaC |
Terraform + Ansible |
|
Security |
Trivy в каждом pipeline |
Часть 1 — Установка кластера
5 VM и Ansible вместо ручной настройки
k8s-master 192.168.11.101 control-plane 2 CPU / 4GB RAM
k8s-node-1 192.168.11.102 worker 2 CPU / 4GB RAM
k8s-node-2 192.168.11.103 worker 2 CPU / 4GB RAM
k8s-node-3 192.168.11.104 worker 2 CPU / 4GB RAM
k8s-node-4 192.168.11.105 worker 2 CPU / 4GB RAM
Написал Ansible playbook с тремя ролями. Запуск:
ansible-playbook -i inventory.ini install-k8s.yml
Ключевые задачи k8s-common (выполняется на каждой ноде):
- name: Disable swap permanently
ansible.builtin.replace:
path: /etc/fstab
regexp: '^([^#].*sswaps.*)$'
replace: '# 1'
- name: Load kernel modules
ansible.builtin.modprobe:
name: "{{ item }}"
loop: [overlay, br_netfilter]
- name: Set sysctl for Kubernetes networking
ansible.posix.sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
sysctl_file: /etc/sysctl.d/k8s.conf
reload: true
loop:
- { key: net.bridge.bridge-nf-call-iptables, value: "1" }
- { key: net.bridge.bridge-nf-call-ip6tables, value: "1" }
- { key: net.ipv4.ip_forward, value: "1" }
- name: Configure containerd with SystemdCgroup
# Ubuntu 22.04 использует cgroups v2 — обязательно включить SystemdCgroup
ansible.builtin.shell: |
containerd config default > /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
- name: Install Kubernetes 1.29 (hold versions)
ansible.builtin.apt:
name: [kubelet=1.29.*, kubeadm=1.29.*, kubectl=1.29.*]
state: present
Ubuntu 22.04 + cgroups v2: Без
SystemdCgroup = trueв containerd kubelet не стартует. На Ubuntu 24.04 поведение аналогичное.
Calico CNI: почему IPIP, а не BGP
Calico поддерживает несколько режимов передачи трафика:
|
Режим |
Как работает |
Когда использовать |
|---|---|---|
|
IPIP |
Инкапсулирует pod-трафик в IP |
Везде, не требует L2 между нодами |
|
VXLAN |
Инкапсуляция в UDP |
Когда IPIP блокирован файрволом |
|
Native BGP |
Прямая маршрутизация |
Физическое железо с BGP-роутером |
Я выбрал IPIP — работает в VMware без дополнительных настроек.
Важно понимать: Calico использует BGP в обоих режимах (IPIP и VXLAN) — для обмена маршрутами между нодами (node-to-node mesh, iBGP). Но сам трафик подов при этом идёт через туннели, а не нативно. BGP здесь — не про внешнюю маршрутизацию, а про синхронизацию таблиц маршрутов внутри кластера.
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.3/manifests/calico.yaml
Грабли #1 — MTU который убил TLS
После установки Calico ноды перешли в Ready. Попытался задеплоить приложение — pods ушли в ImagePullBackOff.
kubectl describe pod product-service-xxx | grep -A5 Warning
# Warning Failed Failed to pull image "ghcr.io/...": EOF
Начал копать. Токены — в порядке. Права — нормально. Ничего очевидного.
Диагностика MTU:
# Смотрим MTU физического интерфейса
ip link show ens33
# ens33: mtu 1500
# Смотрим MTU IPIP туннеля
ip link show tunl0
# tunl0: mtu 1480
# Проверяем прохождение пакетов разного размера
ping -M do -s 1400 192.168.11.102 # OK
ping -M do -s 1450 192.168.11.102 # message too long ← вот оно
Что происходит:
Физический MTU = 1500
IPIP добавляет заголовок = 20 байт
Эффективный MTU для pod трафика = 1480
TLS Certificate пакет ≈ 1460 байт
С IPIP заголовком: 1460 + 20 = 1480 — на грани
При фрагментации TLS разрывается → EOF
Фикс — MTU 1350 с запасом:
# Постоянно через netplan (Ubuntu 22.04)
nano /etc/netplan/00-installer-config.yaml
network:
ethernets:
ens33:
dhcp4: false
addresses: [192.168.11.102/24]
mtu: 1350 # ← добавить эту строку
routes:
- to: default
via: 192.168.11.1
netplan apply
После этого ImagePullBackOff пропал мгновенно. Полдня дебажить, чтобы поменять одно число.
Часть 2 — Сеть и доступ извне
MetalLB: реальный IP для bare-metal
В облаке Service: LoadBalancer получает внешний IP автоматически. В bare-metal кластере он навсегда в <pending>. MetalLB выдаёт IP из указанного пула через L2 ARP:
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.9/config/manifests/metallb-native.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
spec:
addresses:
- 192.168.11.200-192.168.11.210
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: l2adv
namespace: metallb-system
Ограничение L2 режима: при отказе ноды-лидера ARP переключение занимает до минуты, часть трафика теряется. Для production на физическом железе предпочтительнее режим BGP с поддержкой роутера. В моём окружении VMware L2 достаточно.
Ingress-NGINX получил IP 192.168.11.200. Приложение доступно как обычный сайт.
Часть 3 — GitOps и CI/CD
ArgoCD: от push до Running за 2 минуты
git push
↓
GitHub Actions: build → push image → update tag в k8s/base/deployment.yaml → git commit
↓
ArgoCD видит изменение манифеста (polling каждые 3 мин или webhook)
↓
kubectl apply автоматически
↓
Новая версия запущена
Грабли #2 — git push rejected и почему моё решение — костыль
При активном GitOps возникает гонка: GitHub Actions обновляет манифест и пушит коммит. Если в этот момент пушишь ты — rejected.
Моё решение для pet-проекта:
git stash && git pull --rebase && git stash pop && git push
Почему это костыль: в команде git pull --rebase может перезаписать чужие коммиты. Это временное решение, приемлемое когда разработчик один.
Как правильно:
-
ArgoCD Image Updater — следит за новыми тегами в registry и обновляет образ без изменения манифеста в репозитории. Никаких конфликтов.
-
Pull Request + auto-merge — GitHub Actions открывает PR с новым тегом, автоматически мержит после проверок.
Для pet-проекта с одним разработчиком rebase работает. Для команды — используйте Image Updater.
Часть 4 — Observability и база данных
Prometheus + Grafana
helm install kube-prometheus prometheus-community/kube-prometheus-stack
--namespace monitoring
--create-namespace
--version 65.1.1 # фиксируем версию чарта
На дашборде видно CPU/RAM по каждой ноде, запросы к сервисам, состояние подов product-service, order-service.
PostgreSQL HA через CloudNativePG
helm install cnpg cloudnative-pg/cloudnative-pg
--namespace cnpg-system
--create-namespace
--version 0.21.0 # фиксируем версию
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgres
spec:
instances: 3 # 1 primary + 2 replica
storage:
size: 10Gi
Потоковая репликация, автоматический failover при падении Primary.
Часть 5 — Проблемы и планы
Грабли #3 — Calico BGP и недоступная нода
В какой-то момент поды на k8s-node-4 стали недоступны с других нод. Поды живые, трафик не доходит.
Диагностика:
# Статус BGP сессий на проблемной ноде
kubectl exec -n calico-system calico-node-XXXXX -- birdcl show protocols | grep BGP
# BGP сессии с другими нодами — Established
# BGP с node-4 — Active (не Established)
# Проверяем запущен ли BGP демон на node-4
ssh ubuntu@192.168.11.105 "ss -tulpn | grep 179"
# tcp LISTEN 0 128 *:179 *:* users:(("bird",pid=1234))
# Демон запущен и слушает — значит проблема в сети, не в демоне
# Смотрим логи calico-node на проблемной ноде
kubectl logs -n calico-system calico-node-XXXXX | grep -i bgp | tail -20
# ... BGP session with 192.168.11.105 went down: Hold timer expired
# Hold timer expired — keepalive пакеты не доходят
# Проверяем потерю пакетов между нодами
ping -c 100 192.168.11.105 | tail -2
# 8 packets transmitted, 8 received, 0% packet loss ← обычный ping OK
# Но BGP keepalive на порту 179
nc -zv 192.168.11.105 179 # работает
# Проблема именно в нестабильности под нагрузкой на VMware VMNet
Вывод: bird запущен, порт слушает, обычный ping работает. Но BGP keepalive пакеты периодически теряются именно в VMware VMNet под нагрузкой — Hold timer истекает, сессия рвётся.
Временное решение — перенести критичные поды (PostgreSQL) на стабильные ноды:
spec:
template:
spec:
nodeSelector:
kubernetes.io/hostname: k8s-node-2
Правильное решение — перейти на VXLAN режим Calico, который не зависит от стабильности BGP для маршрутизации трафика. В планах.
Что ещё не сделано
|
Проблема |
Текущее состояние |
План |
|---|---|---|
|
MTU persistence |
Только node-1 через netplan, остальные — runtime |
Ansible для всех нод |
|
Calico VXLAN |
Используется IPIP, BGP нестабилен на VMware |
Migrate to VXLAN |
|
Бэкапы |
Нет |
Velero + MinIO |
|
TLS/HTTPS |
HTTP |
Cert-Manager + Let’s Encrypt |
|
Secrets |
Plaintext в манифестах |
Sealed Secrets |
|
GitOps конфликты |
git rebase (костыль) |
ArgoCD Image Updater |
Перфекционизм — враг прогресса. Лучше работающая система с известными слабостями, чем идеальный план который никогда не запустится.
Итог
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# k8s-master Ready control-plane 42d v1.29.4
# k8s-node-1 Ready <none> 42d v1.29.4
# k8s-node-2 Ready <none> 42d v1.29.4
# k8s-node-3 Ready <none> 42d v1.29.4
# k8s-node-4 Ready <none> 42d v1.29.4
kubectl get pods -n ecommerce
# NAME READY STATUS
# frontend-65bb4b9d8d-wlxbk 1/1 Running
# order-service-64d768ddff-2gqf4 1/1 Running
# postgres-0 1/1 Running
# product-service-67cf48889b-9gmtr 1/1 Running
# user-service-5b7bbf799b-5gvwd 1/1 Running
GitHub репозитории:
-
🏗️ cloud-shop — полная инфраструктура + приложение
-
⚙️ ansible-k8s — Ansible playbook: один запуск → готовый кластер
-
🔄 github-actions-templates — готовые CI/CD шаблоны
Если сталкивались с похожими проблемами или есть вопросы — пишите в комментариях!
Автор: ConstantinT
