50 KiB
K3S Ansible Stack
Полный Kubernetes стек на базе K3S с HA (High Availability), управляемый через Ansible внутри Docker-контейнера. Ansible устанавливать не нужно — всё работает через
make.
Содержание
- Архитектура
- Требования
- Структура проекта
- Полная установка с нуля
- Рабочий процесс
- Настройка кластера
- Опциональные компоненты
- Все команды Make
- Тестирование через Molecule
- Компоненты стека
- Raspberry Pi
- Обновление K3S
- Диагностика
- Примеры манифестов
- Решение проблем
Архитектура
Кластер работает в HA-режиме с embedded etcd: все три ноды являются полноценными мастерами. Raspberry Pi участвует в etcd-кворуме, но не принимает рабочие нагрузки.
┌──────────────────────────────────────────────────────────────┐
│ Локальная сеть 192.168.1.0/24 │
│ │
│ VIP: 192.168.1.100 (kube-vip) │
│ ├── :6443 K3S API Server (HA) │
│ └── :80/:443 ingress-nginx → приложения │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ master01 │ │ worker01 │ │ rpi01 │ │
│ │ 192.168.1.10 │ │ 192.168.1.11 │ │ .1.12 │ │
│ │ x86_64 │ │ x86_64 │ │ aarch64 │ │
│ │ │ │ │ │ │ │
│ │ K3S server │ │ K3S server │ │ K3S server │ │
│ │ etcd #1 │ │ etcd #2 │ │ etcd #3 │ │
│ │ cluster-init │ │ kube-vip │ │ NoSchedule │ │
│ │ NFS server │ │ workloads ✓ │ │ workloads ✗│ │
│ │ workloads ✓ │ │ │ │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
│ │
│ StorageClass: nfs-client (default) │
│ └── /srv/nfs/k8s на master01 │
└──────────────────────────────────────────────────────────────┘
Роли нод
| Нода | K3S роль | etcd | Workloads | Описание |
|---|---|---|---|---|
| master01 | server | #1 (leader) | ✓ | Первый сервер, инициализирует кластер (cluster-init) |
| worker01 | server | #2 | ✓ | Присоединяется к master01, запускает поды |
| rpi01 | server | #3 | ✗ | Мастер-нода для quorum, taint NoSchedule |
При отказе любой одной ноды кластер продолжает работать — etcd сохраняет кворум (2 из 3).
Компоненты
| Компонент | Версия | Описание |
|---|---|---|
| K3S | v1.29.3+k3s1 | Лёгкий Kubernetes с embedded etcd (HA) |
| kube-vip | v0.8.3 | VIP для API + LoadBalancer сервисов |
| NFS Server | — | Персистентное хранилище на master01 |
| CSI NFS Driver | v4.8.0 | Динамические PVC через NFS |
| ingress-nginx | chart 4.10.1 | HTTP/S Ingress controller |
| Istio | 1.22.2 | Service mesh (опционально) |
| Kiali | 1.86.0 | UI для Istio, вход по токену (опционально) |
| kube-prometheus-stack | 60.3.0 | Prometheus + Grafana + Alertmanager (опционально) |
| Helm | 3.14.4 | Внутри Docker контейнера |
| kubectl | v1.29.3 | Внутри Docker контейнера |
Требования
На твоей машине (откуда запускаешь)
| Инструмент | Версия | Установка |
|---|---|---|
| Docker | >= 24.0 | https://docs.docker.com/get-docker/ |
| make | любая | apt install make / brew install make |
| SSH ключ | — | ssh-keygen -t ed25519 |
Ansible, Helm, kubectl — устанавливать не нужно, они внутри Docker.
Для Molecule-тестирования дополнительно нужен Python 3.9+:
pip install -r requirements-python.txt
На серверах кластера
| Требование | Описание |
|---|---|
| ОС | Ubuntu 20.04/22.04/24.04, Debian 11/12, Raspberry Pi OS 64-bit |
| SSH | Доступ с твоего публичного ключа |
| sudo | Без пароля (NOPASSWD) — рекомендуется |
| Интернет | Для скачивания K3S, образов, Helm чартов |
| RAM | master01/worker01: 2+ ГБ; rpi01: 1+ ГБ |
Настроить sudo без пароля на каждом сервере:
echo "$USER ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/$USER
Структура проекта
k3s-ansible/
├── Makefile ← Все команды (единая точка входа)
├── Dockerfile ← Образ: Ansible + Helm + kubectl
├── docker-compose.yml
├── .env.example ← Шаблон переменных окружения
├── .yamllint.yml ← Правила линтинга YAML
│
├── requirements.yml ← Ansible Galaxy коллекции
├── requirements-python.txt ← Python пакеты (ansible + molecule)
│
├── ansible.cfg
├── site.yml ← Главный плейбук (serial: 1)
├── upgrade.yml
├── uninstall.yml
├── healthcheck.yml
│
├── inventory/
│ └── hosts.ini ← IP и параметры серверов
│
├── group_vars/all/
│ ├── main.yml ← Все переменные кластера + опции
│ └── vault.yml ← Зашифрованные секреты (токены, пароли)
│
├── host_vars/
│ ├── master01/main.yml ← Labels, server args
│ ├── worker01/main.yml ← Labels, server args
│ └── rpi01/main.yml ← Labels, taint NoSchedule, ARM args
│
└── roles/
├── k3s/ ← K3S HA cluster (embedded etcd)
│ ├── tasks/
│ │ ├── main.yml ← Точка входа
│ │ ├── prereqs.yml ← Пакеты, sysctl, swap, модули ядра
│ │ ├── install_server.yml ← Установка K3S server
│ │ ├── node_config.yml ← Labels и taints
│ │ └── kubeconfig.yml ← Скачать kubeconfig локально
│ ├── templates/
│ │ └── k3s-server-config.yaml.j2
│ └── molecule/default/ ← Unit-тесты роли
│ ├── molecule.yml
│ ├── prepare.yml
│ ├── converge.yml
│ └── verify.yml
│
├── kube-vip/ ← Virtual IP + LoadBalancer
├── nfs-server/ ← NFS сервер
├── csi-nfs/ ← CSI Driver + StorageClass
├── ingress-nginx/ ← Ingress controller
│
├── istio/ ← Service mesh + Kiali (опционально)
│ ├── tasks/main.yml
│ ├── templates/
│ │ ├── istiod-values.yaml.j2
│ │ ├── kiali-values.yaml.j2
│ │ ├── kiali-token-secret.yaml.j2
│ │ └── peer-authentication.yaml.j2
│ └── molecule/default/
│
└── prometheus-stack/ ← Prometheus + Grafana + Alert (опционально)
├── tasks/main.yml
├── templates/
│ └── prometheus-stack-values.yaml.j2
└── molecule/default/
Полная установка с нуля
Это полный сценарий от чистой машины до работающего кластера. Скопируй и выполни блоками.
Шаг 1 — Подготовка серверов
Выполни на каждом сервере (замени user и IP на свои):
# Создать пользователя с sudo без пароля (если нужно)
sudo adduser ansible
echo "ansible ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/ansible
# Или для текущего пользователя:
echo "$USER ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/$USER
Шаг 2 — SSH ключи
# Создать ключ (если нет)
ssh-keygen -t ed25519 -C "k3s-ansible" -f ~/.ssh/id_ed25519
# Скопировать на каждый сервер
ssh-copy-id ubuntu@192.168.1.10 # master01
ssh-copy-id ubuntu@192.168.1.11 # worker01
ssh-copy-id pi@192.168.1.12 # rpi01
# Проверить
ssh ubuntu@192.168.1.10 "hostname"
Шаг 3 — Клонировать и настроить проект
git clone <url> k3s-ansible && cd k3s-ansible
# Создать .env
make setup
Отредактируй .env — задай пароль от vault:
# .env
VAULT_PASSWORD=придумай-надёжный-пароль
Шаг 4 — Инвентарь
Отредактируй inventory/hosts.ini:
[k3s_master]
master01 ansible_host=192.168.1.10 ansible_user=ubuntu
worker01 ansible_host=192.168.1.11 ansible_user=ubuntu
rpi01 ansible_host=192.168.1.12 ansible_user=pi ansible_python_interpreter=/usr/bin/python3
[k3s_cluster:children]
k3s_master
[k3s_cluster:vars]
ansible_ssh_private_key_file=~/.ssh/id_ed25519
[nfs_server]
master01
Шаг 5 — Переменные кластера
Отредактируй group_vars/all/main.yml. Обязательные поля:
kube_vip_address: "192.168.1.100" # свободный IP, не в DHCP пуле
kube_vip_interface: "eth0" # узнать: ssh ubuntu@192.168.1.10 "ip -br a"
Шаг 6 — Vault с секретами
# Сгенерировать токен K3S
openssl rand -hex 32
# → a3f8c2d1e9b04756...
make vault-create
В открывшемся редакторе введи (замени значения на свои):
vault_k3s_token: "a3f8c2d1e9b04756..." # токен из openssl выше
vault_grafana_user: "admin" # если планируешь Prometheus
vault_grafana_password: "мой-пароль"
vault_kiali_token: "" # заполнишь после установки Istio
Сохрани: :wq (в vim) или Ctrl+X → Y (в nano).
Шаг 7 — Собрать Docker образ
make build
# Занимает ~3-5 минут при первом запуске
Шаг 8 — Прогнать тесты (рекомендуется)
Перед деплоем убедись что роли корректны:
# Установить зависимости для Molecule (один раз)
pip install -r requirements-python.txt
# Запустить все тесты (~10-15 минут)
make molecule-all
Если всё зелёное — можно деплоить. Если есть ошибки — смотри раздел Тестирование через Molecule.
Шаг 9 — Проверить SSH и dry-run
# Проверить доступность всех нод
make ping
# Ожидаемый вывод: SUCCESS для каждой ноды
# Проверить плейбук без применения изменений
make check
Шаг 10 — Развернуть базовый стек
make install
Плейбук выполняет всё последовательно (serial: 1):
- master01: prereqs → K3S server (cluster-init) → ждёт готовности API → kube-vip
- worker01: prereqs → K3S server (join) → ждёт готовности ноды
- rpi01: prereqs + cgroups → K3S server (join) → применяет taint NoSchedule
- NFS server на master01
- CSI NFS Driver на всех нодах
- ingress-nginx через Helm
- Финальная проверка: nodes / pods / svc / storageclass
Ожидаемое время: 15-25 минут в зависимости от скорости интернета.
Шаг 11 — Проверить результат
make verify
# Или с локальным kubectl:
export KUBECONFIG=$(pwd)/kubeconfig
kubectl get nodes -o wide
# NAME STATUS ROLES AGE VERSION
# master01 Ready control-plane,etcd,master 5m v1.29.3+k3s1
# worker01 Ready control-plane,etcd,master 3m v1.29.3+k3s1
# rpi01 Ready control-plane,etcd,master 1m v1.29.3+k3s1
kubectl get svc -n ingress-nginx
# NAME TYPE EXTERNAL-IP PORT(S)
# ingress-nginx-controller LoadBalancer 192.168.1.100 80:xxx/443:xxx
kubectl get storageclass
# NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE
# nfs-client (default) nfs.csi.k8s.io Delete Immediate
Шаг 12 (опционально) — Установить Istio + мониторинг
# Добавь в group_vars/all/main.yml:
# istio_enabled: true
# kiali_enabled: true
# prometheus_stack_enabled: true
make install-istio
make install-monitoring
# Или всё сразу с флагами:
# EXTRA_VARS="istio_enabled=true kiali_enabled=true prometheus_stack_enabled=true" make install
Рабочий процесс
Рекомендуемый порядок при любых изменениях в ролях:
Изменить роль → Тест Molecule → Линтинг → Dry-run → Deploy
# 1. Внёс изменение в roles/k3s/ или roles/prometheus-stack/
# 2. Запустить тест только нужной роли
make molecule-k3s # или molecule-prometheus / molecule-istio
# 3. Линтинг всего проекта
make molecule-lint
# 4. Проверить что плейбук парсится корректно (не применяет изменения)
make check
# 5. Деплой на реальные серверы
make install
Тегированный деплой (обновить только один компонент)
make install ANSIBLE_TAGS=k3s # только K3S
make install ANSIBLE_TAGS=kube_vip # только kube-vip
make install ANSIBLE_TAGS=nfs # NFS + CSI
make install ANSIBLE_TAGS=ingress # ingress-nginx
make install ANSIBLE_TAGS=istio # Istio + Kiali
make install ANSIBLE_TAGS=monitoring # Prometheus stack
Настройка кластера
K3S
# group_vars/all/main.yml
k3s_version: "v1.29.3+k3s1"
k3s_cluster_cidr: "10.42.0.0/16"
k3s_service_cidr: "10.43.0.0/16"
k3s_flannel_backend: "vxlan" # vxlan | wireguard-native | host-gw
k3s_disable_traefik: true # ОБЯЗАТЕЛЬНО true при ingress-nginx
kube-vip
kube_vip_address: "192.168.1.100" # Свободный IP — обязательно задать!
kube_vip_interface: "eth0" # Интерфейс master01 (ip a)
kube_vip_mode: "arp" # arp (L2) для домашних сетей | bgp (L3)
NFS / CSI
nfs_exports:
- path: /srv/nfs/k8s
options: "*(rw,sync,no_subtree_check,no_root_squash)"
nfs_allowed_network: "192.168.1.0/24"
csi_nfs_reclaim_policy: "Delete" # Delete | Retain
ingress-nginx
ingress_nginx_service_type: "LoadBalancer"
ingress_nginx_load_balancer_ip: "" # "" = авто от kube-vip
ingress_nginx_set_default_class: true
Индивидуальные настройки нод (host_vars/)
master01 — дополнительные labels и server args:
# host_vars/master01/main.yml
k3s_node_labels:
- "node-role=master"
- "disk-type=ssd"
k3s_extra_server_args: |
kube-controller-manager-arg: "node-monitor-grace-period=20s"
rpi01 — taint запрещает планирование обычных подов:
# host_vars/rpi01/main.yml
k3s_node_taints:
- "node-type=raspberry-pi:NoSchedule"
k3s_extra_server_args: |
kubelet-arg:
- "kube-reserved=cpu=50m,memory=128Mi"
- "system-reserved=cpu=50m,memory=128Mi"
Чтобы разрешить поды на RPi — очисти список:
k3s_node_taints: []
Ansible Vault
make vault-create # Создать
make vault-edit # Редактировать
make vault-view # Просмотреть
make vault-encrypt-string STR="токен" NAME="vault_k3s_token"
Полный шаблон group_vars/all/vault.yml:
# Обязательно:
vault_k3s_token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Если используешь Prometheus stack:
vault_grafana_user: "admin"
vault_grafana_password: "мой-безопасный-пароль"
# Если используешь Kiali (заполни после первой установки):
vault_kiali_token: ""
Опциональные компоненты
Включаются через переменные в group_vars/all/main.yml. По умолчанию все отключены — базовый стек работает без них.
Istio (Service Mesh)
Устанавливает istio/base (CRDs) → istiod (control plane) → istio/gateway (LoadBalancer) → глобальную политику mTLS.
Включить:
# group_vars/all/main.yml
istio_enabled: true
Или разово без изменения файлов:
make install-istio
Параметры:
| Переменная | Умолч. | Описание |
|---|---|---|
istio_enabled |
false |
Включить установку |
istio_version |
1.22.2 |
Версия Helm chart |
istio_mtls_mode |
STRICT |
Режим mTLS: STRICT / PERMISSIVE / DISABLE |
istio_install_gateway |
true |
Устанавливать Ingress Gateway |
istio_telemetry_enabled |
true |
Сбор метрик для Prometheus |
Kiali (UI для Istio)
Веб-интерфейс для визуализации service mesh. Требует istio_enabled: true.
Включить:
kiali_enabled: true
Аутентификация по токену:
Kiali настроен со стратегией token — для входа в UI нужен Kubernetes ServiceAccount токен.
При первой установке Ansible:
- Создаёт ServiceAccount
kiali-adminс правамиcluster-admin - Создаёт Secret типа
kubernetes.io/service-account-token - Читает сгенерированный k8s токен и выводит его в конце плейбука
══════════════════════════════════════════════════
Kiali UI: kubectl -n istio-system port-forward svc/kiali 20001:20001
Откройте: http://localhost:20001
Стратегия: token
Токен для входа:
eyJhbGciOiJSUzI1NiIsImtpZCI6...
Сохрани токен в vault.yml: vault_kiali_token: <токен>
══════════════════════════════════════════════════
Скопируй выведенный токен в vault:
make vault-edit
# добавь: vault_kiali_token: "eyJhbGciOiJSUzI1NiIsImtpZCI6..."
При последующих запусках Ansible использует сохранённый токен и не создаёт новый.
Параметры:
| Переменная | Умолч. | Описание |
|---|---|---|
kiali_enabled |
false |
Включить установку |
kiali_version |
1.86.0 |
Версия Helm chart |
kiali_token |
vault_kiali_token |
Токен входа (из vault) |
kiali_ingress_enabled |
false |
Создать Ingress |
kiali_ingress_host |
kiali.local |
Hostname для Ingress |
Включить Ingress:
kiali_ingress_enabled: true
kiali_ingress_host: "kiali.example.com"
kiali_ingress_class: "nginx"
kube-prometheus-stack (Prometheus + Grafana + Alertmanager)
Полный monitoring-стек: Prometheus, Grafana, Alertmanager, node-exporter, kube-state-metrics. Данные хранятся на PVC через StorageClass nfs-client.
Включить:
prometheus_stack_enabled: true
Или разово:
make install-monitoring
Хранилище (PVC):
| Компонент | Переменная | Умолч. | Назначение |
|---|---|---|---|
| Prometheus | prometheus_storage_size |
10Gi |
Метрики временных рядов |
| Grafana | grafana_storage_size |
5Gi |
Дашборды, плагины, настройки |
| Alertmanager | prometheus_alertmanager_storage_size |
2Gi |
Состояние алертов |
Изменить размер:
# group_vars/all/main.yml
prometheus_storage_size: "20Gi"
grafana_storage_size: "10Gi"
prometheus_alertmanager_storage_size: "5Gi"
# Явно указать StorageClass (по умолчанию используется default = nfs-client):
# prometheus_storage_class: "nfs-client"
# grafana_storage_class: "nfs-client"
Grafana — доступ:
После установки Grafana доступна на NodePort 32000 (на любой ноде):
http://192.168.1.10:32000
Логин и пароль из vault:
vault_grafana_user: "admin"
vault_grafana_password: "мой-пароль"
Включить Ingress вместо NodePort:
prometheus_grafana_ingress_enabled: true
prometheus_grafana_ingress_host: "grafana.example.com"
Все параметры:
| Переменная | Умолч. | Описание |
|---|---|---|
prometheus_stack_enabled |
false |
Включить установку |
prometheus_stack_version |
60.3.0 |
Версия Helm chart |
prometheus_retention_days |
7 |
Срок хранения метрик (дней) |
prometheus_storage_size |
10Gi |
PVC Prometheus |
grafana_storage_enabled |
true |
Включить PVC для Grafana |
grafana_storage_size |
5Gi |
PVC Grafana |
grafana_admin_user |
vault_grafana_user |
Логин Grafana |
prometheus_grafana_admin_password |
vault_grafana_password |
Пароль Grafana |
prometheus_alertmanager_enabled |
true |
Включить Alertmanager |
prometheus_alertmanager_storage_size |
2Gi |
PVC Alertmanager |
prometheus_node_exporter_enabled |
true |
Метрики хостов (DaemonSet) |
prometheus_kube_state_metrics_enabled |
true |
Метрики объектов k8s |
Интеграция Kiali ↔ Prometheus/Grafana:
При istio_enabled: true + prometheus_stack_enabled: true Kiali автоматически получает URL Prometheus и Grafana и настраивается на их использование без дополнительной конфигурации.
Все команды Make
Настройка и сборка
make setup # Создать .env из шаблона
make build # Собрать Docker образ (~5 мин)
make rebuild # Пересобрать без кэша
Проверки
make ping # SSH до всех нод
make check # Dry-run без изменений
make lint # Проверить синтаксис плейбуков
Установка
make install # Полный базовый стек
make install-k3s # Только K3S HA кластер
make install-kubevip # Только kube-vip
make install-nfs # NFS + CSI
make install-ingress # ingress-nginx
make install-istio # Istio + Kiali (нужен istio_enabled: true в vars)
make install-monitoring # Prometheus + Grafana (нужен prometheus_stack_enabled: true)
Тестирование (Molecule)
make molecule-k3s # Тест роли k3s (~5-8 мин)
make molecule-prometheus # Тест роли prometheus-stack (~2-3 мин)
make molecule-istio # Тест роли istio (~2-3 мин)
make molecule-all # Все тесты последовательно (~15 мин)
make molecule-lint # Только линтинг YAML+ansible-lint (без Docker, ~30 сек)
Обновление и диагностика
make upgrade VERSION=v1.30.0+k3s1 # Обновить K3S
make health # Полная диагностика
make verify # Сводка стека
Vault
make vault-create
make vault-edit
make vault-view
make vault-encrypt-string STR=... NAME=...
Прочее
make shell # bash внутри ansible-контейнера
make uninstall # Удалить весь стек (с подтверждением)
make clean # Удалить Docker образ
make clean-all # Образ + kubeconfig + кэш Docker
Переменные командной строки
ANSIBLE_VERBOSITY=2 make install # Подробный вывод
ANSIBLE_TAGS=k3s,kube_vip make install # Только теги
EXTRA_VARS="k3s_version=v1.30.0+k3s1" make install-k3s # Доп. переменные
Тестирование через Molecule
Molecule — стандартный инструмент для тестирования Ansible ролей. Каждая роль запускается в Docker-контейнере, проходит набор автоматических проверок и удаляется. Реальные серверы при этом не нужны.
Установка
# Установить зависимости (один раз)
pip install -r requirements-python.txt
# Проверить
molecule --version
# molecule 6.x.x ...
docker --version
# Docker version 24.x.x ...
Жизненный цикл теста
Команда molecule test выполняет следующие фазы по порядку:
dependency → скачать зависимые Ansible-роли (если есть)
lint → yamllint + ansible-lint (статический анализ)
syntax → ansible-playbook --syntax-check
create → запустить Docker-контейнер (платформа)
prepare → подготовить контейнер (установить Python, collections)
converge → выполнить тестируемые задачи внутри контейнера
idempotency → выполнить converge повторно (проверить что нет лишних изменений)
verify → запустить assertions (проверить результаты)
cleanup → очистить состояние (если задан cleanup.yml)
destroy → удалить Docker-контейнер
При ошибке на любой фазе — тест падает, контейнер удаляется автоматически.
Что тестирует каждая роль
Роль k3s (privileged Docker, ~5-8 мин)
Использует Docker-контейнер с systemd в привилегированном режиме — это необходимо для тестирования apt, sysctl и модулей ядра.
Тестируемые задачи:
prereqs.yml— установка пакетов (curl,ca-certificates,iptables), отключение swap, загрузка модулей ядра (overlay,br_netfilter), настройка sysctl
Рендеринг шаблона:
k3s-server-config.yaml.j2→/etc/rancher/k3s/config.yaml
Что проверяет verify.yml:
| Проверка | Что именно |
|---|---|
Директория /etc/rancher/k3s |
Создана роль prereqs |
Файл config.yaml |
Существует и имеет права 0600 |
| YAML синтаксис | Файл парсится как корректный YAML |
token |
Установлен из переменной |
cluster-cidr |
Равен 10.42.0.0/16 |
service-cidr |
Равен 10.43.0.0/16 |
cluster-init: true |
Первый мастер инициализирует кластер |
disable: [traefik] |
Traefik выключен (используется ingress-nginx) |
curl установлен |
Системный пакет |
iptables установлен |
Системный пакет |
net.ipv4.ip_forward = 1 |
sysctl параметр применён |
Роль prometheus-stack (lightweight Docker, ~2-3 мин)
Только рендеринг Jinja2-шаблона — не требует privileged режима.
Тестируемые задачи:
- Рендеринг
prometheus-stack-values.yaml.j2→/tmp/prometheus-stack-values.yaml
Что проверяет verify.yml:
| Проверка | Что именно |
|---|---|
grafana.adminUser |
Значение переменной grafana_admin_user |
grafana.adminPassword |
Непустой пароль из переменной |
grafana.persistence.enabled |
true — PVC включён |
grafana.persistence.size |
5Gi — размер PVC Grafana |
prometheus.prometheusSpec.retention |
7d |
| Prometheus PVC storage | 10Gi |
| Alertmanager enabled | true |
| Alertmanager PVC storage | 2Gi |
| nodeExporter enabled | true |
Роль istio (lightweight Docker, ~2-3 мин)
Рендеринг всех четырёх шаблонов роли.
Тестируемые задачи:
istiod-values.yaml.j2→/tmp/istiod-values.yamlkiali-values.yaml.j2→/tmp/kiali-values.yamlpeer-authentication.yaml.j2→/tmp/peer-authentication.yamlkiali-token-secret.yaml.j2→/tmp/kiali-token-secret.yaml
Что проверяет verify.yml:
| Файл | Проверка |
|---|---|
istiod-values.yaml |
Ресурсы pilot (cpu/memory), meshConfig существует, enablePrometheusMerge: true |
kiali-values.yaml |
auth.strategy: token, Prometheus URL содержит prom-kube-prometheus-stack-prometheus, Grafana auth username |
peer-authentication.yaml |
kind: PeerAuthentication, spec.mtls.mode: STRICT |
kiali-token-secret.yaml |
type: kubernetes.io/service-account-token, аннотация kiali-admin |
Запуск тестов: пошагово
Быстрая проверка (lint, без Docker)
make molecule-lint
Запускает yamllint . и ansible-lint на всём проекте. Выполняется за ~30 секунд, Docker не нужен. Используй перед каждым коммитом.
Запуск линтинга...
yamllint .
ansible-lint
✓ Линтинг прошёл
Тест одной роли
make molecule-k3s
Вывод (успешный):
INFO default scenario test matrix: dependency, lint, cleanup, destroy, syntax, create, prepare, converge, idempotency, verify, cleanup, destroy
INFO Performing prerun with role_name_check=1...
INFO Running default > create
INFO Sanity checks: 'docker'
INFO Running default > prepare
PLAY [Prepare k3s test environment] ****
TASK [Wait for systemd to start] ***
ok: [k3s-node]
TASK [Install Python3] ***
changed: [k3s-node]
INFO Running default > converge
PLAY [Converge — k3s role unit tests] ****
TASK [Mock k3s binary] ***
changed: [k3s-node]
TASK [Test prereqs — install packages] ***
changed: [k3s-node] => (item=curl)
changed: [k3s-node] => (item=iptables)
TASK [Test server config template rendering] ***
changed: [k3s-node]
INFO Running default > idempotency
PLAY [Converge — k3s role unit tests] ****
...
PLAY RECAP
k3s-node : ok=12 changed=0 unreachable=0 failed=0
INFO Idempotency completed successfully.
INFO Running default > verify
PLAY [Verify — k3s role] ****
TASK [Assert config directory] ***
ok: [k3s-node] => {"changed": false, "msg": "All assertions passed"}
TASK [Assert config file exists] ***
ok: [k3s-node] => {"changed": false, "msg": "All assertions passed"}
TASK [Assert cluster-init is set] ***
ok: [k3s-node] => {"changed": false, "msg": "All assertions passed"}
TASK [Assert traefik is disabled] ***
ok: [k3s-node] => {"changed": false, "msg": "All assertions passed"}
TASK [Assert ip_forward is 1] ***
ok: [k3s-node] => {"changed": false, "msg": "All assertions passed"}
TASK [Summary] ***
ok: [k3s-node] => {
"msg": "Все проверки прошли успешно для ноды k3s-node"
}
INFO Running default > destroy
INFO Pruning extra files from scenario ephemeral directory
✓ k3s role: OK
Все тесты
make molecule-all
Тестирую роль k3s...
...
✓ k3s role: OK
Тестирую роль prometheus-stack...
...
✓ prometheus-stack role: OK
Тестирую роль istio...
...
✓ istio role: OK
✓ Все тесты прошли успешно
Отладка упавших тестов
Если тест упал — контейнер удаляется автоматически. Чтобы оставить контейнер живым для ручной отладки, используй отдельные фазы:
# Перейти в директорию роли
cd roles/prometheus-stack
# Только создать контейнер и запустить задачи (без удаления)
molecule converge
# Проверить вручную внутри контейнера
molecule login
# Теперь ты внутри Docker-контейнера:
cat /tmp/prometheus-stack-values.yaml
python3 -c "import yaml; yaml.safe_load(open('/tmp/prometheus-stack-values.yaml'))"
exit
# Запустить только verify (assertions)
molecule verify
# Посмотреть полный вывод с -vvv
molecule converge -- -vvv
# Удалить контейнер когда закончил
molecule destroy
Типичные ошибки и решения
| Ошибка | Причина | Решение |
|---|---|---|
Unable to pull image |
Нет интернета или Docker Hub | Проверь подключение, попробуй docker pull geerlingguy/docker-ubuntu2204-ansible |
FAILED: assert ... is defined |
Переменная не задана в converge.yml |
Добавь переменную в секцию vars: в converge.yml |
Idempotency: CHANGED |
Таск меняет состояние при повторном запуске | Добавь changed_when: false или исправь идемпотентность задачи |
yamllint: wrong indentation |
Ошибка отступа в YAML | Исправь файл, запусти make molecule-lint |
ansible-lint: no-changed-when |
Таск shell/command без changed_when |
Добавь changed_when: <условие> к задаче |
sysctl: Operation not permitted |
Контейнер не privileged | Убедись что в molecule.yml стоит privileged: true |
Запуск конкретной фазы вручную
cd roles/k3s
molecule lint # только линтинг
molecule syntax # только синтаксис
molecule create # только создать контейнер
molecule prepare # только prepare.yml
molecule converge # только converge.yml (основной тест)
molecule idempotency # только проверка идемпотентности
molecule verify # только verify.yml (assertions)
molecule destroy # только удалить контейнер
Написание новых тестов
Структура каждого scenario:
molecule.yml — конфигурация драйвера и платформы:
driver:
name: docker
platforms:
- name: my-test-instance
image: geerlingguy/docker-ubuntu2204-ansible:latest
pre_build_image: true
privileged: true # нужно для sysctl, модулей ядра
groups:
- k3s_master # добавить в Ansible-группу для шаблонов
provisioner:
name: ansible
verifier:
name: ansible
converge.yml — что запускать (не весь role, а конкретные задачи):
- name: Converge
hosts: all
become: true
vars:
my_var: "test-value" # все нужные переменные задаются здесь
tasks:
- name: Render template
ansible.builtin.template:
src: "{{ playbook_dir }}/../../templates/my-template.j2"
dest: /tmp/result.yaml
verify.yml — что проверять:
- name: Verify
hosts: all
tasks:
- name: Read result
ansible.builtin.slurp:
src: /tmp/result.yaml
register: raw
- name: Parse YAML
ansible.builtin.set_fact:
result: "{{ raw.content | b64decode | from_yaml }}"
- name: Assert key exists
ansible.builtin.assert:
that: result.myKey == 'expected-value'
fail_msg: "Ожидалось 'expected-value', получено: {{ result.myKey }}"
Компоненты стека
K3S HA (embedded etcd)
Три ноды в режиме server формируют etcd-кластер из 3 участников — кворум сохраняется при отказе любой одной ноды.
Порядок установки управляется serial: 1 в site.yml:
master01 (cluster-init: true)
↓ готов, API отвечает на :6443
worker01 (server: https://192.168.1.10:6443)
↓ присоединился, etcd = 2/3
rpi01 (server: https://192.168.1.10:6443, NoSchedule taint)
↓ присоединился, etcd = 3/3, кворум достигнут
Конфигурационный файл K3S генерируется из шаблона k3s-server-config.yaml.j2. Шаблон автоматически определяет роль ноды:
# master01 получает:
cluster-init: true
# worker01 и rpi01 получают:
server: "https://192.168.1.10:6443"
kube-vip
Создаёт виртуальный IP через ARP (L2). VIP анонсируется всем устройствам в локальной сети. При отказе мастера VIP автоматически мигрирует на другую ноду в течение нескольких секунд.
Требования к VIP:
- В той же подсети что серверы
- Не занят другим устройством
- Не выдаётся DHCP сервером
NFS + CSI
NFS сервер разворачивается на master01. Каждый PVC создаёт отдельную папку внутри /srv/nfs/k8s:
/srv/nfs/k8s/
├── default/ ← namespace
│ └── my-pvc/ ← имя PVC
│ └── pvc-xxx-yyy-zzz/ ← имя PV
│ └── данные...
NFS можно вынести на отдельный хост — задай в inventory/hosts.ini:
[nfs_server]
nfshost ansible_host=192.168.1.20 ansible_user=ubuntu
И в group_vars/all/main.yml:
csi_nfs_server: "192.168.1.20"
ingress-nginx
Устанавливается через Helm, отключает встроенный Traefik K3S, получает LoadBalancer IP от kube-vip. Настроен с JSON-логами, CORS, proxy timeouts и tolerations для всех типов нод (включая control-plane и RPi).
Raspberry Pi
Роль автоматически определяет ARM-архитектуру и:
- Включает
memory cgroupв/boot/cmdline.txt(или/boot/firmware/cmdline.txtдля Bookworm) и перезагружает RPi - Применяет пониженные kubelet-резервы (
cpu=50m, memory=128Mi) - Настраивает пороги garbage collection образов (85%/80%)
- Применяет taint
node-type=raspberry-pi:NoSchedule— поды не планируются если нет явного toleration
Рекомендуемая ОС: Raspberry Pi OS Lite 64-bit (Bookworm) для RPi 4/5.
Для деплоя на RPi добавь toleration в манифест:
tolerations:
- key: "node-type"
operator: "Equal"
value: "raspberry-pi"
effect: "NoSchedule"
Чтобы снять ограничение и использовать RPi как обычную ноду — очисти host_vars/rpi01/main.yml:
k3s_node_taints: []
Docker образ
Образ собирается один раз через make build и содержит всё необходимое для управления кластером:
FROM python:3.12-slim-bookworm
├── openssh-client, curl, jq, git
├── ansible-core 2.16, ansible 9.x
├── kubernetes, openshift Python пакеты
├── Helm 3.14.4
├── kubectl v1.29.3
└── Ansible Collections:
├── community.general >= 8.0
├── ansible.posix >= 1.5
└── kubernetes.core >= 3.0
При запуске монтируются:
| Путь в контейнере | Источник | Назначение |
|---|---|---|
/ansible |
$(PWD) |
Весь проект |
/root/.ssh |
~/.ssh (read-only) |
SSH ключи |
Обновление K3S
make upgrade VERSION=v1.30.0+k3s1
Происходит по очереди (serial: 1 в upgrade.yml):
- Drain ноды → вытеснить поды
- Обновить K3S бинарник
- Перезапустить сервис
- Дождаться
Ready - Uncordon → восстановить планирование
- Перейти к следующей ноде
HA-режим гарантирует доступность кластера во время обновления: пока одна нода обновляется, остальные две обслуживают трафик.
Диагностика
make health # Статус сервисов, поды, диск, память
make verify # Nodes, pods, services, storageclass, ingress
# Открыть shell в ansible-контейнере:
make shell
# Внутри контейнера:
ansible all -m shell -a "systemctl status k3s"
ansible k3s_master -m shell -a "k3s kubectl get nodes -o wide"
ansible k3s_master -m shell -a "k3s kubectl get pods -A | grep -v Running"
ansible k3s_master -m shell -a "k3s etcd-snapshot ls"
# Локально с kubectl (после make install):
export KUBECONFIG=$(pwd)/kubeconfig
kubectl top nodes
kubectl top pods -A
kubectl get events -A --sort-by='.lastTimestamp' | tail -20
Проверка etcd
# Статус участников etcd
kubectl -n kube-system exec -it \
$(kubectl -n kube-system get pods -l component=etcd -o name | head -1) \
-- etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/var/lib/rancher/k3s/server/tls/etcd/server-ca.crt \
--cert=/var/lib/rancher/k3s/server/tls/etcd/server-client.crt \
--key=/var/lib/rancher/k3s/server/tls/etcd/server-client.key \
member list
Примеры манифестов
Приложение с Ingress и NFS-хранилищем
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
accessModes: [ReadWriteMany]
storageClassName: nfs-client
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 2
selector:
matchLabels: {app: my-app}
template:
metadata:
labels: {app: my-app}
spec:
containers:
- name: app
image: nginx:alpine
volumeMounts:
- mountPath: /data
name: storage
volumes:
- name: storage
persistentVolumeClaim:
claimName: app-data
---
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
selector: {app: my-app}
ports: [{port: 80}]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
spec:
ingressClassName: nginx
rules:
- host: myapp.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 80
Приложение в Istio mesh
apiVersion: v1
kind: Namespace
metadata:
name: my-app
labels:
istio-injection: enabled # автоматически внедрять sidecar
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: my-app
namespace: my-app
spec:
hosts: ["myapp.local"]
gateways: ["istio-system/istio-ingressgateway"]
http:
- route:
- destination:
host: my-app
port:
number: 80
Приложение только на x86 нодах (не на RPi)
spec:
template:
spec:
nodeSelector:
node-type: x86_64
Приложение с доступом к Grafana/Prometheus (ServiceMonitor)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: my-app
namespace: my-app
labels:
release: prom # должен совпадать с prometheus_stack_release_name
spec:
selector:
matchLabels:
app: my-app
endpoints:
- port: metrics
interval: 30s
Решение проблем
| Проблема | Решение |
|---|---|
Permission denied (publickey) |
ssh-copy-id user@server |
Vault decryption failed |
Проверь VAULT_PASSWORD в .env |
| kube-vip VIP не пингуется | Проверь kube_vip_interface — должен совпадать с ip a на master01 |
| RPi K3S не стартует | ssh pi@rpi "cat /proc/cgroups | grep memory" — должна быть 1 |
| PVC в Pending | kubectl get events -A — проверь монтирование NFS на нодах |
ingress EXTERNAL-IP <pending> |
kubectl -n kube-system get pods | grep kube-vip — поды kube-vip должны быть Running |
| etcd нет кворума | Нужно минимум 2 из 3 нод. kubectl get nodes — проверь статус |
| worker01/rpi01 не присоединились | Убедись что master01 полностью стартовал. serial: 1 гарантирует порядок, но нода должна быть доступна |
| Kiali: "Could not get token" | Проверь: kubectl -n istio-system get secret kiali-admin-token |
| Grafana PVC Pending | kubectl get sc — nfs-client должен быть (default). Проверь NFS сервер: exportfs -v |
| Molecule: image pull failed | docker pull geerlingguy/docker-ubuntu2204-ansible:latest вручную |
| Molecule: idempotency failed | Таск выдаёт changed при повторном запуске — добавь changed_when: false |
ansible-lint ошибки |
Запусти make molecule-lint и исправь указанные файлы |
Отладка с подробным выводом
ANSIBLE_VERBOSITY=2 make install # основной вывод
ANSIBLE_VERBOSITY=4 make install # максимальный вывод (SSH, модули)
ANSIBLE_VERBOSITY=2 ANSIBLE_TAGS=k3s make install # подробно только для k3s
Сброс и повторная установка
# Удалить весь стек (с подтверждением)
make uninstall
# Затем установить заново
make install