Предыстория
Когда изучаешь 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 RAMk8s-node-1 192.168.11.102 worker 2 CPU / 4GB RAMk8s-node-2 192.168.11.103 worker 2 CPU / 4GB RAMk8s-node-3 192.168.11.104 worker 2 CPU / 4GB RAMk8s-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: '^([^#].*\sswap\s.*)$' 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 # OKping -M do -s 1450 192.168.11.102 # message too long ← вот оно
Что происходит:
Физический MTU = 1500IPIP добавляет заголовок = 20 байтЭффективный MTU для pod трафика = 1480TLS 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/v1beta1kind: IPAddressPoolspec: addresses: - 192.168.11.200-192.168.11.210---apiVersion: metallb.io/v1beta1kind: L2Advertisementmetadata: 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/v1kind: Clustermetadata: name: postgresspec: 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-4ssh 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 на порту 179nc -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.4kubectl 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 шаблоны
Если сталкивались с похожими проблемами или есть вопросы — пишите в комментариях!
ссылка на оригинал статьи https://habr.com/ru/articles/1041356/