Stateless ArgoCD для bare-metal Kubernetes на Proxmox

Stateless ArgoCD для Bare-Metal Kubernetes

Kubernetes-кластер на 64 ГБ ОЗУ за €39 в месяц — часть 6

В предыдущей статье мы построили граничный уровень кластера.

На хосте Proxmox настроена двойная схема HAProxy: она принимает трафик на публичный IP и перенаправляет его на рабочие узлы за NAT. Дальше управление берёт на себя HAProxy Ingress Controller и маршрутизирует запросы к сервисам внутри кластера. Эта схема закрыла сетевую часть лабораторного стенда.

Теперь пришло время решить вопрос с развёртыванием приложений.

В этой статье мы устанавливаем ArgoCD в bare-metal Kubernetes-кластер на Proxmox.

Вместо стандартной конфигурации Helm мы запускаем контроллер ArgoCD в режиме без состояния (stateless mode), чтобы ускорить восстановление после отказа узлов.

Такая настройка рассчитана на кластеры, использующие локальное NVMe-хранилище с local-path-provisioner, где инфраструктурные компоненты должны свободно перемещаться между узлами.

В этом руководстве рассмотрим:

  • почему стандартный StatefulSet ArgoCD может приводить к проблемам

  • как запустить контроллер как Deployment

  • как ускорить восстановление после отказа узла

  • как это вписывается в GitOps-процесс

Архитектурная схема stateless-установки ArgoCD на виртуальных машинах Proxmox: один узел переживает сбой

Архитектура хранилища для высоконагруженных Kubernetes-рабочих нагрузок

Этот Kubernetes-кластер создан для аналитических систем с высокой интенсивностью записи, включая такие нагрузки, как:

  • Kafka

  • ClickHouse

  • PostgreSQL

  • аналитические пайплайны

Подобные системы генерируют большой объём операций записи, а значит, пропускная способность диска и задержки имеют принципиальное значение.

Именно поэтому кластер намеренно обходится без распределённых систем хранения, таких как:

  • Ceph

  • Longhorn

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

Вместо этого кластер предоставляет локальное NVMe-хранилище напрямую через local-path-provisioner.

Это делает путь к хранилищу простым и быстрым.

Многие распределённые системы — Kafka, ClickHouse и PostgreSQLуже реализуют репликацию на уровне приложения, поэтому дополнительная репликация на уровне хранилища излишня.

Компромисс состоит в том, что тома становятся привязанными к конкретному узлу (node-local).

Если узел пропадёт, поды, использующие его тома, не смогут автоматически переехать на другой узел.

Для stateful-нагрузок это ожидаемое поведение.

Однако для инфраструктурных сервисов вроде ArgoCD такое поведение недопустимо.

Почему ArgoCD должен быть stateless

ArgoCD выступает плоскостью управления GitOps для кластера.

Он следит за Git, сравнивает желаемое состояние с реальным состоянием кластера и устраняет расхождения.

Если контроллер перестаёт работать, кластер продолжает функционировать — но процесс согласования (reconciliation) останавливается.

Изменения, отправленные в Git, не применятся. Дрейф конфигурации не исправится.

Иными словами, ArgoCD должен вести себя как stateless-инфраструктура управления, а не как нагрузка, привязанная к конкретному узлу.

К сожалению, стандартная модель развёртывания не всегда обеспечивает такое поведение.

Почему контроллеры ArgoCD могут зависнуть в состоянии Terminating

По умолчанию Helm-чарт ArgoCD разворачивает контроллер как StatefulSet.

StatefulSet гарантирует:

  • стабильные идентификаторы подов

  • упорядоченный запуск и завершение

  • сохранение идентичности каждого пода

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

Проблема возникает при отказе узлов.

Представьте, что рабочий узел внезапно пропадает из-за:

  • аппаратного сбоя

  • проблем с сетью

Под контроллера, запущенный на этом узле, может перейти в состояние Terminating.

Kubernetes пытается завершить под корректно (gracefully).

Однако kubelet на этом узле больше недоступен.

Процесс завершения так и не заканчивается.

С точки зрения API-сервера Kubernetes под всё ещё существует.

Поскольку StatefulSet требует полного исчезновения предыдущего пода перед созданием нового, Kubernetes отказывается запускать замену контроллера.

Кластер продолжает работать, но согласование в ArgoCD полностью останавливается.

Единственное решение — ручное вмешательство:

kubectl delete pod argocd-application-controller-0 --force --grace-period=0 -n argocd

До тех пор пока кто-то не заметит проблему, GitOps-автоматизация фактически не работает.

Как запустить ArgoCD без состояния в Kubernetes

Изучая стандартное развёртывание ArgoCD, я проанализировал шаблоны официального Helm-чарта.

Примечательно, что чарт не предоставляет прямой опции для запуска контроллера как Deployment.

По умолчанию он всегда разворачивает контроллер как StatefulSet, рассчитывая на стабильные идентификаторы подов и классические конфигурации кластеров.

Тем не менее в чарте есть альфа-функция dynamicClusterDistribution.

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

Но под капотом она меняет тип рабочей нагрузки контроллера со StatefulSet на Deployment.

Это поведение включается следующей конфигурацией:

controller:
  dynamicClusterDistribution: true
  replicas: 1

Это позволяет Kubernetes запланировать замену контроллера на другом узле, даже если предыдущий под завис в состоянии Terminating на отказавшем узле.

Для развёртываний с одной репликой (replicas: 1) логика динамического шардинга кластеров фактически не активна.

На практике этот флаг просто даёт возможность запустить контроллер как stateless Deployment.

Ускорение восстановления после отказа узла

Ещё один важный параметр конфигурации — тайминги вытеснения узлов (node eviction timing).

Когда Kubernetes обнаруживает, что узел недоступен, он не сразу вытесняет поды.

По умолчанию Kubernetes ждёт около пяти минут, прежде чем перепланировать нагрузки.

Для инфраструктурных сервисов вроде ArgoCD такая задержка излишня.

Сокращение окна допуска (toleration window) ускоряет восстановление после сбоев.

global:
  tolerations:
    - key: node.kubernetes.io/unreachable
      operator: Exists
      effect: NoExecute
      tolerationSeconds: 90
    - key: node.kubernetes.io/not-ready
      operator: Exists
      effect: NoExecute
      tolerationSeconds: 90

С такой конфигурацией поды переезжают примерно через 90 секунд вместо пяти минут.

Это существенно повышает устойчивость плоскости управления GitOps.

Установка ArgoCD в Kubernetes-кластер

Чтобы установку можно было воспроизвести, в репозитории есть вспомогательный скрипт для установки ArgoCD.

kubespray/install_argocd.sh

Скрипт устанавливает ArgoCD через Helm, используя пользовательскую stateless-конфигурацию.

#!/usr/bin/env bash
# Strict mode:
# -e exit on any command failure
# -u fail on undefined variables
# -o pipefail fail if any command in a pipeline fails
set -euo pipefail
NAMESPACE="argocd"
VALUES_FILE="values-ha-stateless.yaml"
echo "🚀 Adding ArgoCD Helm repository..."
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
echo "📦 Installing/Upgrading ArgoCD in namespace $NAMESPACE..."
helm upgrade --install argocd argo/argo-cd \
  --namespace $NAMESPACE \
  --create-namespace \
  -f argocd/$VALUES_FILE
echo "✅ Installation completed. Checking pods..."
kubectl get pods -n $NAMESPACE

Файл конфигурации находится по пути kubespray/argocd/values_ha_stateless.yaml.

Он содержит описанную выше stateless-конфигурацию, включая изменение типа развёртывания контроллера и ускоренные настройки вытеснения узлов:

# High Availability (HA) Stateless ArgoCD Configuration
#
# This configuration addresses a critical edge case: with default ArgoCD installation,
# when worker nodes are shut down or become unreachable, ArgoCD pods can hang on
# those nodes for up to 5 minutes (default tolerationSeconds), causing service
# unavailability. This configuration reduces eviction time to 90 seconds and ensures
# proper pod distribution across nodes for high availability.
#
global:
  # Fast controller eviction from dead/unreachable nodes (90 seconds)
  #
  # PROBLEM: Default ArgoCD tolerations allow pods to stay on unreachable/not-ready
  # nodes for 300 seconds (5 minutes). When workers are shut down for maintenance,
  # ArgoCD pods remain scheduled on those nodes, causing:
  # - Service unavailability (UI/API unreachable)
  # - Sync operations fail
  # - No automatic recovery until node comes back or timeout expires
  #
  # SOLUTION: Reduce tolerationSeconds to 90 seconds so Kubernetes evicts pods
  # from dead nodes quickly and reschedules them on healthy nodes.
  #
  # This ensures ArgoCD remains available even when nodes are shut down for
  # maintenance or in case of node failures.
  tolerations:
    - key: "node.kubernetes.io/unreachable"
      operator: "Exists"
      effect: "NoExecute"
      tolerationSeconds: 90 # Evict after 90s instead of default 300s
    - key: "node.kubernetes.io/not-ready"
      operator: "Exists"
      effect: "NoExecute"
      tolerationSeconds: 90 # Evict after 90s instead of default 300s
controller:
  # Convert controller from StatefulSet to Deployment for stateless operation
  #
  # CRITICAL ISSUE WITH STATEFULSET: StatefulSet can hang indefinitely when a node
  # becomes unreachable:
  #
  # - When a node with StatefulSet pod becomes unreachable, the pod enters
  #   "Terminating" state but cannot be fully deleted because Kubernetes cannot
  #   communicate with the node to gracefully terminate the pod
  # - StatefulSet will NOT create a replacement pod until the old pod is fully
  #   deleted (unlike Deployment which can create new pods immediately)
  # - If the node never comes back or is permanently lost, the StatefulSet pod
  #   can remain in "Terminating" state forever, leaving the service unavailable
  # - Manual intervention (force delete pod, delete node object) is required
  #
  #
  # WHY THIS NON-STANDARD CONFIGURATION:
  #
  # ArgoCD Helm chart does NOT provide a direct option to use Deployment instead
  # of StatefulSet. The chart's architecture assumes StatefulSet by default for
  # the controller, which makes sense for traditional deployments with persistent
  # storage and stable network identities.
  #
  # However, the chart exposes an ALPHA feature called "dynamicClusterDistribution"
  # that happens to use Deployment under the hood. This feature was designed for
  # dynamic cluster sharding (rebalancing managed clusters across controller
  # replicas), but it also changes the controller from StatefulSet to Deployment.
  #
  # FEATURE IS STABLE ENOUGH: Despite being marked "alpha", the Deployment
  # behavior itself is stable. The "alpha" label refers to the dynamic sharding
  # algorithm, not the Deployment vs StatefulSet choice. For single-replica
  # deployments (replicas: 1), the sharding aspect is irrelevant anyway.
  #
  # NOTE: With replicas: 1, the dynamic sharding feature is effectively disabled
  # (no clusters to distribute), so we're only using this flag to get Deployment
  # behavior, not for its intended sharding purpose.
  dynamicClusterDistribution: true
  replicas: 1
repoServer:
  # Repository server handles Git repository operations (clone, fetch, etc.)
  #
  # WHY 3 REPLICAS: High availability - if one node fails, other repo servers
  # continue serving requests. With 3 replicas and podAntiAffinity, we ensure
  # one repo server per node, so a single node failure doesn't take down all
  # repository operations.
  #
  # POD ANTI-AFFINITY: Ensures repo servers are distributed across different
  # nodes. This prevents all replicas from being on the same node, which would
  # cause complete service loss if that node is shut down (the edge case we're
  # solving). With requiredDuringSchedulingIgnoredDuringExecution, Kubernetes
  # will not schedule multiple repo servers on the same node.
  replicas: 3 # One per node for HA
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchLabels:
              app.kubernetes.io/name: argocd-repo-server
          topologyKey: "kubernetes.io/hostname" # Distribute across nodes
server:
  # ArgoCD server provides UI and API endpoints
  #
  # WHY 3 REPLICAS: High availability for UI/API access. With 3 replicas and
  # podAntiAffinity, we ensure the server is always available even when a node
  # is shut down. Load balancer can route traffic to remaining healthy instances.
  #
  # WITHOUT THIS: Default single replica on one node means if that node is shut
  # down, ArgoCD UI/API becomes completely unavailable until the node returns or
  # pod is manually rescheduled (the edge case we're solving).
  #
  # POD ANTI-AFFINITY: Distributes server instances across nodes, preventing
  # all servers from being on the same node. Combined with fast tolerations,
  # ensures service remains available during node maintenance or failures.
  replicas: 3 # UI/API always available without waiting for migration
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchLabels:
              app.kubernetes.io/name: argocd-server
          topologyKey: "kubernetes.io/hostname" # Distribute across nodes
redis:
  # Redis cache for ArgoCD session storage and caching
  #
  # STATELESS CONFIGURATION: Disable persistent volumes for stateless operation
  #
  # NOTE: According to official ArgoCD chart, redis.persistentVolume.enabled: false
  # disables persistent volumes. The redis-ha subchart will use default storage
  # (emptyDir) when persistent volumes are disabled.
  enabled: true
  # Disable persistent volumes for stateless operation
  persistentVolume:
    enabled: false

Конфигурация включает:

  • stateless-развёртывание контроллера

  • высокодоступные серверы репозиториев

  • распределённые API-серверы

  • кэш Redis в оперативной памяти

Запуск установки

Сначала подготовьте окружение:

source prepare_env.sh
cd kubespray/

Запустите tmux-сессию, чтобы SSH-туннели и проброс портов работали в фоне.

tmux new -s kube-lab

В первом окне запустите SSH-туннель:

./ssh_port_forwarding.sh

Нажмите Ctrl+B → C, чтобы создать новое окно tmux, и запустите установку ArgoCD:

./install_argocd.sh

Скрипт выполняет следующие шаги:

  • добавляет Helm-репозиторий ArgoCD

  • устанавливает ArgoCD с конфигурацией из values_ha_stateless.yaml

  • разворачивает все компоненты в пространстве имён argocd

После завершения вы увидите контроллер, repo-server, API-сервер и поды Redis, распределённые по узлам кластера.

Доступ к веб-интерфейсу ArgoCD

Чтобы открыть интерфейс ArgoCD локально:

kubectl port-forward svc/argocd-server -n argocd 8080:443

Затем откройте в браузере http://localhost:8080.

Получите исходный пароль администратора:

kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d;
echo

Войдите с данными:

  • Имя пользователя: admin

  • Пароль: значение, полученное из секрета выше

Почему stateless ArgoCD повышает устойчивость кластера

При стандартной конфигурации StatefulSet отказ узла может оставить контроллер ArgoCD в зависшем состоянии на неопределённый срок.

Запуск контроллера как Deployment меняет процесс восстановления:

  1. Kubernetes обнаруживает отказ узла

  2. Поды вытесняются по истечении окна допуска

  3. Новый под контроллера запускается на другом узле

  4. Согласование GitOps возобновляется автоматически

Для кластеров, использующих локальное NVMe-хранилище и репликацию на уровне приложений, поддержание инфраструктурных компонентов в stateless-режиме существенно повышает надёжность.

Что дальше и как автоматизировать процесс

Теперь, когда ArgoCD работает в кластере, следующий шаг — подключить его к Git-репозиторию и настроить автоматическое управление ресурсами кластера.

Репозиторий на GitHub обновлён: клонируйте репозиторий, переключитесь на ветку part_9 и следуйте инструкциям в README.md.

➜ В следующей статье мы познакомимся с Argo CD и перейдём на Git-based рабочий процесс развёртывания.

В следующей статье мы создадим первое приложение ArgoCD и полностью переведём кластер на GitOps-процесс.

С этого момента развёртывание приложений превращается в коммит вместо kubectl apply.

© 2026 meganuke