Files
K3S/README.md
Sergey Antropoff 24846d2e52 feat: крупное обновление стека — пути, bootstrap, etcd, cert-manager, custom errors, ноды
## Переименование путей (rancher → kubernetes)

- Все пути /var/lib/rancher/k3s → /var/lib/kubernetes/k3s
- Все пути /etc/rancher/k3s   → /etc/kubernetes/k3s
- Добавлены переменные k3s_config_dir, k3s_data_dir, k3s_kubeconfig_path
- K3S install получил --data-dir и K3S_CONFIG_FILE флаги
- k3s-server-config.yaml.j2: добавлены write-kubeconfig и data-dir ключи
- Все роли (csi-nfs, ingress-nginx, cert-manager, prometheus, istio, cni)
  переведены на {{ k3s_kubeconfig_path }} вместо хардкода

## Bootstrap (новое)

- bootstrap.yml — playbook для первоначальной настройки нод
- roles/bootstrap/ — создаёт пользователя ansible, настраивает sudoers,
  деплоит SSH публичный ключ по паролю из vault
- host_vars/*/vault.yml.example — шаблоны с bootstrap_user/bootstrap_password
- make bootstrap, make vault-bootstrap-create NODE=..., make vault-bootstrap-edit NODE=...

## Добавление/удаление нод (новое)

- add-node.yml — добавляет мастер или воркер в существующий кластер через VIP
- remove-node.yml — cordon → drain → delete → uninstall → cleanup
- inventory/hosts.ini: добавлена группа [k3s_workers], обновлён [k3s_cluster:children]
- roles/k3s/tasks/main.yml: install_agent.yml для воркеров

## etcd backup/restore (новое)

- etcd-backup.yml / etcd-restore.yml — top-level playbooks
- roles/etcd/tasks/backup.yml — k3s etcd-snapshot save + retention cleanup
- roles/etcd/tasks/restore.yml — cluster-reset + перезапуск всех нод
- make etcd-backup, make etcd-restore SNAPSHOT=..., make etcd-list-snapshots

## cert-manager addon (новое)

- roles/cert-manager/ — установка через Helm + опциональный ClusterIssuer
- Поддержка: none | selfsigned | letsencrypt
- Шаблоны ClusterIssuer для selfsigned CA и ACME HTTP-01
- Управляется флагом cert_manager_enabled: false

## Custom error backend для ingress-nginx (новое)

- custom-error-page.html.j2 — тёмная Kubernetes-styled страница ошибок
- custom-error-backend.yaml.j2 — ConfigMap + Deployment (nginx) + Service
- nginx использует sub_filter для динамической подстановки X-Code/X-Message
- ingress-nginx Helm values: custom-http-errors, default-backend-service
- Управляется флагом ingress_nginx_custom_errors_enabled: true

## Hostname и пакеты (новое)

- prereqs.yml: установка hostname из inventory_hostname (пропускается в Molecule)
- prereqs.yml: установка k3s_common_packages (nfs-common, mc, htop, vim, wget, и др.)
- molecule_test: true в converge.yml исключает hostname из тестов

## Molecule improvements

- 3 платформы: master01 (Ubuntu 22.04) + worker01 (Ubuntu 22.04) + rpi01 (Debian 12)
- Molecule запускается из Docker контейнера через /var/run/docker.sock (DinD)
- Все пути в converge.yml и verify.yml обновлены под /etc/kubernetes/k3s

## Флаги включения компонентов

- kube_vip_enabled, nfs_server_enabled, csi_nfs_enabled, ingress_nginx_enabled
- cert_manager_enabled, istio_enabled, kiali_enabled, prometheus_stack_enabled
- Каждая роль пропускает установку через meta: end_play при disabled

## Документация

- README полностью переработан: все новые возможности с примерами
- Новые разделы: Управление нодами, etcd backup/restore, cert-manager, bootstrap
- Обновлены Makefile, docker/entrypoint.sh под все новые команды
2026-04-23 06:32:14 +03:00

1705 lines
65 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# K3S Ansible Stack
> Полный Kubernetes стек на базе K3S с HA (High Availability), управляемый через Ansible внутри Docker-контейнера.
> Ansible устанавливать **не нужно** — всё работает через `make`.
## Содержание
- [Архитектура](#архитектура)
- [Требования](#требования)
- [Структура проекта](#структура-проекта)
- [Полная установка с нуля](#полная-установка-с-нуля)
- [Рабочий процесс](#рабочий-процесс)
- [Настройка кластера](#настройка-кластера)
- [Опциональные компоненты](#опциональные-компоненты)
- [Управление нодами](#управление-нодами)
- [Резервное копирование etcd](#резервное-копирование-etcd)
- [Все команды Make](#все-команды-make)
- [Тестирование через Molecule](#тестирование-через-molecule)
- [Компоненты стека](#компоненты-стека)
- [Raspberry Pi](#raspberry-pi)
- [Обновление K3S](#обновление-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-master01 (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 |
| cert-manager | v1.15.3 | TLS сертификаты (опционально) |
| 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 контейнера |
### CNI плагин
По умолчанию используется встроенный в K3S **Flannel**. Переключение — одна переменная в `group_vars/all/main.yml`:
```yaml
k3s_cni: "flannel" # flannel (по умолч.) | calico | cilium
```
| CNI | Режим | Особенности |
|---|---|---|
| `flannel` | встроен в K3S | VXLAN overlay, минимальная настройка |
| `calico` | Tigera operator | Network Policy, IPAM, BGP, VXLAN/IPIP |
| `cilium` | Helm | eBPF dataplane, Hubble observability, L7 policy |
При `calico` или `cilium` — Flannel автоматически отключается в конфиге K3S (`flannel-backend: none`, `disable-network-policy: true`).
Установить отдельно:
```bash
make install-cni K3S_CNI=calico # или cilium
```
---
## Требования
### На твоей машине (откуда запускаешь)
| Инструмент | Версия | Установка |
|---|---|---|
| **Docker** | >= 24.0 | https://docs.docker.com/get-docker/ |
| **make** | любая | `apt install make` / `brew install make` |
| **SSH ключ** | — | `ssh-keygen -t ed25519` |
Ansible, Helm, kubectl, Molecule — устанавливать **не нужно**. Всё работает внутри Docker-контейнера.
### На серверах кластера
| Требование | Описание |
|---|---|
| ОС | 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 без пароля **на каждом сервере**:
```bash
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
├── add-node.yml ← Добавить ноду в кластер
├── remove-node.yml ← Удалить ноду из кластера
├── etcd-backup.yml ← Создать снимок etcd
├── etcd-restore.yml ← Восстановить etcd из снимка
├── 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
├── cert-manager/ ← TLS сертификаты (опционально)
│ ├── tasks/main.yml
│ ├── defaults/main.yml
│ └── templates/
│ ├── clusterissuer-selfsigned.yaml.j2
│ └── clusterissuer-letsencrypt.yaml.j2
├── etcd/ ← Backup/restore задачи etcd
│ ├── tasks/
│ │ ├── backup.yml
│ │ └── restore.yml
│ └── defaults/main.yml
├── 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 на свои):
```bash
# Создать пользователя с 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 ключи
```bash
# Создать ключ (если нет)
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 — Клонировать и настроить проект
```bash
git clone <url> k3s-ansible && cd k3s-ansible
# Создать .env
make setup
```
Отредактируй `.env` — задай пароль от vault:
```bash
# .env
VAULT_PASSWORD=придумай-надёжный-пароль
```
### Шаг 4 — Инвентарь
Отредактируй `inventory/hosts.ini`:
```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`. Обязательные поля:
```yaml
kube_vip_address: "192.168.1.100" # свободный IP, не в DHCP пуле
# kube_vip_interface: "" # пусто = автоопределение (eth0/enp3s0/end0/...)
# переопредели только если авто не работает
k3s_cni: "flannel" # flannel (умолч.) | calico | cilium
```
`kube_vip_interface` определяется автоматически через `ansible_default_ipv4.interface` — работает для Ubuntu, Debian, Raspberry Pi OS без дополнительных настроек.
### Шаг 6 — Vault с секретами
```bash
# Сгенерировать токен K3S
openssl rand -hex 32
# → a3f8c2d1e9b04756...
make vault-create
```
В открывшемся редакторе введи (замени значения на свои):
```yaml
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 образ
```bash
make build
# Занимает ~3-5 минут при первом запуске
```
### Шаг 8 — Прогнать тесты (рекомендуется)
Перед деплоем убедись что роли корректны. Molecule запускается **внутри Docker** — устанавливать ничего не нужно.
```bash
# Убедись что образ собран (если ещё не сделал на шаге 7):
# make build
# Запустить все тесты (~15-20 минут)
make molecule-all
```
Что поднимается: 3 контейнера (master01/worker01 на Ubuntu 22.04 + rpi01 на Debian 12), тест конфигурации K3S HA, шаблонов Prometheus и Istio.
Если всё зелёное — можно деплоить. Если есть ошибки — смотри раздел [Тестирование через Molecule](#тестирование-через-molecule).
### Шаг 9 — Проверить SSH и dry-run
```bash
# Проверить доступность всех нод
make ping
# Ожидаемый вывод: SUCCESS для каждой ноды
# Проверить плейбук без применения изменений
make check
```
### Шаг 10 — Развернуть базовый стек
```bash
make install
```
Плейбук выполняет всё последовательно (`serial: 1`):
1. master01: prereqs → K3S server (cluster-init) → ждёт готовности API
2. worker01: prereqs → K3S server (join) → ждёт готовности ноды
3. rpi01: prereqs + cgroups → K3S server (join) → применяет taint NoSchedule
4. CNI плагин (если не Flannel — устанавливается Calico или Cilium)
5. kube-vip: VIP + LoadBalancer (интерфейс определяется автоматически)
6. NFS server на master01
7. CSI NFS Driver на всех нодах → StorageClass `nfs-master01`
8. ingress-nginx через Helm
9. cert-manager (только если `cert_manager_enabled: true`)
10. Финальная проверка: nodes / pods / svc / storageclass
Ожидаемое время: **15-25 минут** в зависимости от скорости интернета.
### Шаг 11 — Проверить результат
```bash
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-master01 (default) nfs.csi.k8s.io Delete Immediate
```
### Шаг 12 (опционально) — Установить Istio + мониторинг
```bash
# Добавь в 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
```
```bash
# 1. Внёс изменение в roles/k3s/ или roles/prometheus-stack/
# 2. Запустить тест только нужной роли
make molecule-k3s # или molecule-prometheus / molecule-istio
# 3. Линтинг всего проекта
make molecule-lint
# 4. Проверить что плейбук парсится корректно (не применяет изменения)
make check
# 5. Деплой на реальные серверы
make install
```
### Тегированный деплой (обновить только один компонент)
```bash
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
```yaml
# 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 (только для Flannel)
k3s_disable_traefik: true # ОБЯЗАТЕЛЬНО true при ingress-nginx
# CNI выбирается здесь:
k3s_cni: "flannel" # flannel | calico | cilium
# Пути K3S (изменены с /var/lib/rancher на /var/lib/kubernetes)
k3s_config_dir: /etc/kubernetes/k3s # конфиги и kubeconfig
k3s_data_dir: /var/lib/kubernetes/k3s # данные etcd, токены, манифесты
k3s_kubeconfig_path: "{{ k3s_config_dir }}/k3s.yaml"
```
### CNI
**Flannel** (по умолчанию) — ничего дополнительно настраивать не нужно.
**Calico:**
```yaml
k3s_cni: "calico"
calico_version: "v3.28.0"
calico_encapsulation: "VXLAN" # VXLAN | IPIP | None
```
**Cilium:**
```yaml
k3s_cni: "cilium"
cilium_version: "1.15.5"
cilium_hubble_enabled: true # Hubble observability
cilium_hubble_ui_enabled: false # Hubble UI (ресурсоёмко)
# cilium_k8s_service_host автоматически = kube_vip_address
```
При смене CNI необходима переустановка кластера — CNI нельзя поменять "горячей" заменой.
### kube-vip
```yaml
kube_vip_address: "192.168.1.100" # Свободный IP — обязательно задать!
kube_vip_interface: "" # пусто = автоопределение
# Ansible определит eth0/enp3s0/end0 автоматически
# Переопредели только если нужно: "eth0"
kube_vip_mode: "arp" # arp (L2) для домашних сетей | bgp (L3)
```
Автоопределение интерфейса работает через `ansible_default_ipv4.interface` — корректно определяет интерфейс на Ubuntu, Debian, Raspberry Pi OS. Если у разных нод разные имена интерфейсов — задай `kube_vip_interface` принудительно.
### NFS / CSI
```yaml
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
# StorageClass именуется автоматически: nfs-<hostname NFS сервера>
# Например: nfs-master01 (если NFS на master01)
# Переопредели только если нужно другое имя:
# csi_nfs_storageclass_name: "my-nfs"
```
### ingress-nginx
```yaml
ingress_nginx_service_type: "LoadBalancer"
ingress_nginx_load_balancer_ip: "" # "" = авто от kube-vip
ingress_nginx_set_default_class: true
```
### Индивидуальные настройки нод (`host_vars/`)
**master01** — дополнительные labels и server args:
```yaml
# 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 запрещает планирование обычных подов:
```yaml
# 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 — очисти список:
```yaml
k3s_node_taints: []
```
### Ansible Vault
```bash
make vault-create # Создать
make vault-edit # Редактировать
make vault-view # Просмотреть
make vault-encrypt-string STR="токен" NAME="vault_k3s_token"
```
Полный шаблон `group_vars/all/vault.yml`:
```yaml
# Обязательно:
vault_k3s_token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Если используешь Prometheus stack:
vault_grafana_user: "admin"
vault_grafana_password: "мой-безопасный-пароль"
# Если используешь Kiali (заполни после первой установки):
vault_kiali_token: ""
```
---
## Опциональные компоненты
Включаются через переменные в `group_vars/all/main.yml`. По умолчанию все отключены — базовый стек работает без них.
---
### cert-manager (TLS сертификаты)
Автоматически выдаёт и обновляет TLS сертификаты в кластере. Поддерживает самоподписанные сертификаты (для внутреннего использования) и Let's Encrypt (для публичных доменов).
**Включить:**
```yaml
# group_vars/all/main.yml
cert_manager_enabled: true
```
Или разово:
```bash
make install-cert-manager
```
**Типы ClusterIssuer:**
| `cert_manager_issuer` | Описание |
|---|---|
| `none` | Только установка cert-manager, без ClusterIssuer |
| `selfsigned` | Самоподписанный CA (умолч.) — для внутреннего использования |
| `letsencrypt` | Let's Encrypt ACME — требует публичный домен и ingress-nginx |
**Параметры:**
| Переменная | Умолч. | Описание |
|---|---|---|
| `cert_manager_enabled` | `false` | Включить установку |
| `cert_manager_version` | `v1.15.3` | Версия Helm chart |
| `cert_manager_namespace` | `cert-manager` | Namespace |
| `cert_manager_issuer` | `selfsigned` | Тип ClusterIssuer |
| `cert_manager_acme_email` | `admin@example.com` | Email для Let's Encrypt |
| `cert_manager_acme_server` | `prod` | `prod` или `staging` |
**Использование selfsigned (внутренний CA):**
После установки будут созданы:
- `selfsigned-issuer` — базовый самоподписанный issuer
- `cluster-ca` — Certificate (CA сертификат)
- `cluster-ca-issuer` — ClusterIssuer для выдачи сертификатов приложениям
Пример использования в манифестах:
```yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: my-app-tls
spec:
secretName: my-app-tls-secret
issuerRef:
name: cluster-ca-issuer
kind: ClusterIssuer
dnsNames:
- myapp.local
```
**Использование Let's Encrypt:**
```yaml
cert_manager_issuer: "letsencrypt"
cert_manager_acme_email: "admin@example.com"
cert_manager_acme_server: "prod" # или staging для тестирования
```
Добавить аннотацию на Ingress:
```yaml
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
```
---
### Istio (Service Mesh)
Устанавливает `istio/base` (CRDs) → `istiod` (control plane) → `istio/gateway` (LoadBalancer) → глобальную политику mTLS.
**Включить:**
```yaml
# group_vars/all/main.yml
istio_enabled: true
```
Или разово без изменения файлов:
```bash
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`.
**Включить:**
```yaml
kiali_enabled: true
```
**Аутентификация по токену:**
Kiali настроен со стратегией `token` — для входа в UI нужен Kubernetes ServiceAccount токен.
При первой установке Ansible:
1. Создаёт ServiceAccount `kiali-admin` с правами `cluster-admin`
2. Создаёт Secret типа `kubernetes.io/service-account-token`
3. Читает сгенерированный k8s токен и **выводит его в конце плейбука**
```
══════════════════════════════════════════════════
Kiali UI: kubectl -n istio-system port-forward svc/kiali 20001:20001
Откройте: http://localhost:20001
Стратегия: token
Токен для входа:
eyJhbGciOiJSUzI1NiIsImtpZCI6...
Сохрани токен в vault.yml: vault_kiali_token: <токен>
══════════════════════════════════════════════════
```
Скопируй выведенный токен в vault:
```bash
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:
```yaml
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`.
**Включить:**
```yaml
prometheus_stack_enabled: true
```
Или разово:
```bash
make install-monitoring
```
**Хранилище (PVC):**
| Компонент | Переменная | Умолч. | Назначение |
|---|---|---|---|
| Prometheus | `prometheus_storage_size` | `10Gi` | Метрики временных рядов |
| Grafana | `grafana_storage_size` | `5Gi` | Дашборды, плагины, настройки |
| Alertmanager | `prometheus_alertmanager_storage_size` | `2Gi` | Состояние алертов |
Изменить размер:
```yaml
# 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:
```yaml
vault_grafana_user: "admin"
vault_grafana_password: "мой-пароль"
```
Включить Ingress вместо NodePort:
```yaml
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 и настраивается на их использование без дополнительной конфигурации.
---
## Управление нодами
### Добавить ноду
Перед добавлением:
1. Добавь ноду в `inventory/hosts.ini` в нужную группу (`k3s_master` или `k3s_workers`)
2. Убедись что SSH доступ работает: `make ping`
```bash
# Добавить мастер-ноду (станет etcd участником)
make add-node NODE=master04
# Добавить рабочую ноду (только agent, без etcd)
make add-node NODE=worker04
```
Новые ноды подключаются через **VIP** (`kube_vip_address`) — kube-vip должен быть запущен. После добавления нода автоматически появляется в кластере.
Пример `inventory/hosts.ini` с рабочими нодами:
```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
[k3s_workers]
worker04 ansible_host=192.168.1.14 ansible_user=ubuntu
worker05 ansible_host=192.168.1.15 ansible_user=ubuntu
[k3s_cluster:children]
k3s_master
k3s_workers
```
### Удалить ноду
```bash
make remove-node NODE=worker04
```
Порядок удаления:
1. `cordon` — запретить планирование новых подов
2. `drain` — вытеснить все поды на другие ноды (таймаут 180 сек)
3. `delete` — удалить ноду из Kubernetes API
4. Запустить uninstall-скрипт K3S на удалённой ноде
5. Очистить директории K3S
После удаления убери ноду из `inventory/hosts.ini`.
**Ограничения:**
- Нельзя удалить первый мастер (`groups['k3s_master'][0]`) — он является точкой инициализации кластера
- При удалении мастера etcd-кворум уменьшается: 3 → 2 нод. Рекомендуется добавить новый мастер после удаления
---
## Резервное копирование etcd
etcd хранит **всё состояние кластера** — поды, сервисы, конфигмапы, секреты, PVC. Регулярные снимки позволяют восстановить кластер после сбоя.
Снимки хранятся на сервере по пути `{{ k3s_data_dir }}/server/db/snapshots` (по умолчанию `/var/lib/kubernetes/k3s/server/db/snapshots`).
### Создать снимок
```bash
make etcd-backup
```
Создаёт снимок с автоматическим именем `k3s-etcd-<timestamp>.db`, удаляет старые снимки сверх `etcd_backup_retention` (по умолчанию 5).
Опционально — скопировать снимок на локальную машину:
```yaml
# group_vars/all/main.yml
etcd_backup_copy_to_local: true
etcd_backup_local_dir: "./etcd-backups"
```
### Список снимков
```bash
make etcd-list-snapshots
```
### Восстановить из снимка
```bash
# Посмотреть доступные снимки
make etcd-list-snapshots
# Восстановить (с подтверждением — введи 'yes')
make etcd-restore SNAPSHOT=k3s-etcd-20250101T120000.db
# Без интерактивного подтверждения
FORCE=true make etcd-restore SNAPSHOT=k3s-etcd-20250101T120000.db
```
**Что происходит при восстановлении:**
1. K3S останавливается на **всех** мастерах и воркерах
2. Запускается `k3s server --cluster-reset --cluster-reset-restore-path=<snapshot>` на первом мастере (процесс завершается после сброса)
3. K3S запускается на первом мастере → ждёт готовности API
4. K3S запускается на остальных мастерах
5. K3S-agent запускается на воркерах
**Параметры резервного копирования:**
| Переменная | Умолч. | Описание |
|---|---|---|
| `etcd_backup_dir` | `{{ k3s_data_dir }}/server/db/snapshots` | Директория снимков на сервере |
| `etcd_backup_retention` | `5` | Количество хранимых снимков |
| `etcd_backup_copy_to_local` | `false` | Копировать снимок на Ansible-хост |
| `etcd_backup_local_dir` | `./etcd-backups` | Локальная директория для снимков |
---
## Все команды Make
### Настройка и сборка
```bash
make setup # Создать .env из шаблона
make build # Собрать Docker образ (~5 мин)
make rebuild # Пересобрать без кэша
```
### Проверки
```bash
make ping # SSH до всех нод
make check # Dry-run без изменений
make lint # Проверить синтаксис плейбуков
```
### Установка
```bash
make install # Полный базовый стек
make install-k3s # Только K3S HA кластер
make install-cni # CNI плагин (make install-cni K3S_CNI=calico|cilium)
make install-kubevip # Только kube-vip
make install-nfs # NFS + CSI
make install-ingress # ingress-nginx
make install-cert-manager # cert-manager + ClusterIssuer
make install-istio # Istio + Kiali (нужен istio_enabled: true в vars)
make install-monitoring # Prometheus + Grafana (нужен prometheus_stack_enabled: true)
```
### Управление нодами
```bash
make add-node NODE=worker04 # Добавить ноду в кластер
make remove-node NODE=worker04 # Безопасно удалить ноду
```
### etcd — резервное копирование
```bash
make etcd-backup # Создать снимок etcd
make etcd-list-snapshots # Список доступных снимков
make etcd-restore SNAPSHOT=k3s-etcd-XXX.db # Восстановить из снимка
```
### Тестирование (Molecule — всё в Docker, pip не нужен)
```bash
make molecule-k3s # Тест роли k3s — 3 контейнера (Ubuntu+Debian), ~8-12 мин
make molecule-prometheus # Тест роли prometheus-stack (~2-3 мин)
make molecule-istio # Тест роли istio (~2-3 мин)
make molecule-all # Все тесты последовательно (~15-20 мин)
make molecule-lint # Линтинг YAML+ansible-lint в контейнере (~30 сек)
```
### Обновление и диагностика
```bash
make upgrade VERSION=v1.30.0+k3s1 # Обновить K3S
make health # Полная диагностика
make verify # Сводка стека
```
### Vault
```bash
make vault-create
make vault-edit
make vault-view
make vault-encrypt-string STR=... NAME=...
```
### Прочее
```bash
make shell # bash внутри ansible-контейнера
make uninstall # Удалить весь стек (с подтверждением)
make clean # Удалить Docker образ
make clean-all # Образ + kubeconfig + кэш Docker
```
### Переменные командной строки
```bash
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-контейнерах, проходит набор автоматических проверок и удаляется. Реальные серверы при этом **не нужны**.
### Требования
Только **Docker** — Molecule, Python и зависимости уже внутри образа `k3s-ansible`. Устанавливать ничего дополнительно не нужно.
```bash
# Убедись что образ собран:
make build
# Запустить тесты (всё в Docker):
make molecule-k3s
```
Как это работает: `make molecule-*` запускает контейнер `k3s-ansible` с примонтированным Docker socket (`/var/run/docker.sock`). Внутри этого контейнера Molecule создаёт тестовые контейнеры для каждой роли — **Docker внутри Docker без демона** (только socket от хоста).
### Жизненный цикл теста
Команда `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` (3 контейнера, privileged Docker, ~8-12 мин)
Поднимает **3 контейнера**, точно как в inventory:
| Контейнер | Образ | Соответствует |
|---|---|---|
| `master01` | `geerlingguy/docker-ubuntu2204-ansible` | Ubuntu 22.04 x86_64 |
| `worker01` | `geerlingguy/docker-ubuntu2204-ansible` | Ubuntu 22.04 x86_64 |
| `rpi01` | `geerlingguy/docker-debian12-ansible` | Debian 12 (Raspberry Pi OS) |
Все контейнеры — privileged с systemd (нужно для `apt`, `sysctl`, модулей ядра).
`rpi01` получает `ansible_python_interpreter=/usr/bin/python3` и `NoSchedule` taint — как в реальном inventory.
**Тестируемые задачи:**
- `prereqs.yml` — пакеты, swap, модули ядра, sysctl
- Рендеринг `k3s-server-config.yaml.j2``/etc/kubernetes/k3s/config.yaml`
**Что проверяет `verify.yml`:**
| Проверка | Нода | Что именно |
|---|---|---|
| Директория `/etc/kubernetes/k3s` | все | Создана роль prereqs |
| Файл `config.yaml` | все | Существует, права `0600` |
| `cluster-init: true` | master01 | Первый мастер инициализирует кластер |
| `server: https://...:6443` | worker01, rpi01 | Присоединяются к master01 |
| `cluster-cidr` | все | Равен `10.42.0.0/16` |
| `disable: [traefik]` | все | Traefik выключен |
| `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.yaml`
- `kiali-values.yaml.j2``/tmp/kiali-values.yaml`
- `peer-authentication.yaml.j2``/tmp/peer-authentication.yaml`
- `kiali-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` |
---
### Запуск тестов: пошагово
#### Линтинг (в контейнере, ~30 сек)
```bash
make molecule-lint
```
Запускает `yamllint .` и `ansible-lint` на всём проекте внутри Docker-контейнера. Используй перед каждым коммитом.
```
Запуск линтинга...
✓ Линтинг прошёл
```
#### Тест одной роли
```bash
make molecule-k3s
```
Вывод (успешный, сокращён):
```
INFO Running default > create
INFO Sanity checks: 'docker'
TASK [Create instance(s)] ****
changed: [localhost] => (item=master01)
changed: [localhost] => (item=worker01)
changed: [localhost] => (item=rpi01)
INFO Running default > prepare
TASK [Wait for systemd to start] ***
ok: [master01]
ok: [worker01]
ok: [rpi01]
INFO Running default > converge
TASK [Mock k3s binary] ***
changed: [master01]
changed: [worker01]
changed: [rpi01]
TASK [Test server config template rendering] ***
changed: [master01]
changed: [worker01]
changed: [rpi01]
INFO Running default > idempotency
PLAY RECAP
master01 : ok=12 changed=0 unreachable=0 failed=0
worker01 : ok=12 changed=0 unreachable=0 failed=0
rpi01 : ok=12 changed=0 unreachable=0 failed=0
INFO Idempotency completed successfully.
INFO Running default > verify
TASK [Assert cluster-init is set (только master01)] ***
ok: [master01] => {"msg": "All assertions passed"}
skipping: [worker01]
skipping: [rpi01]
TASK [Assert server URL is set (worker01 и rpi01)] ***
skipping: [master01]
ok: [worker01] => {"msg": "All assertions passed"}
ok: [rpi01] => {"msg": "All assertions passed"}
TASK [Assert ip_forward is 1] ***
ok: [master01]
ok: [worker01]
ok: [rpi01]
INFO Running default > destroy
✓ k3s role: OK
```
#### Все тесты
```bash
make molecule-all
```
```
Тестирую роль k3s (3 ноды: master01, worker01, rpi01)...
...
✓ k3s role: OK
Тестирую роль prometheus-stack...
...
✓ prometheus-stack role: OK
Тестирую роль istio...
...
✓ istio role: OK
Все тесты прошли успешно
```
---
### Отладка упавших тестов
Если тест упал — контейнеры удаляются автоматически. Чтобы оставить контейнеры живыми для ручной отладки, войди в интерактивный shell внутри Molecule-контейнера и запускай отдельные фазы:
```bash
# Открыть shell внутри ansible-runner контейнера с Docker socket
docker run --rm -it \
-v $(pwd):/ansible \
-v /var/run/docker.sock:/var/run/docker.sock \
--entrypoint bash \
k3s-ansible
# Внутри контейнера — перейти к роли и запускать фазы вручную:
cd /ansible/roles/prometheus-stack
molecule converge # создать контейнер и запустить задачи (без удаления)
molecule login # войти в тестовый контейнер
# Внутри: cat /tmp/prometheus-stack-values.yaml
# Внутри: exit
molecule verify # только assertions
molecule converge -- -vvv # подробный вывод
molecule destroy # удалить тестовый контейнер
```
Для роли k3s, которая поднимает 3 контейнера:
```bash
cd /ansible/roles/k3s
molecule converge # поднимет master01, worker01, rpi01
molecule login --host master01 # войти в конкретный контейнер
molecule login --host rpi01
molecule verify
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` |
#### Фазы Molecule (внутри runner-контейнера)
```bash
# Войти в runner-контейнер:
docker run --rm -it -v $(pwd):/ansible -v /var/run/docker.sock:/var/run/docker.sock \
--entrypoint bash k3s-ansible
cd /ansible/roles/k3s # или prometheus-stack / istio
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`** — конфигурация драйвера и платформы:
```yaml
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, а конкретные задачи):
```yaml
- 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`** — что проверять:
```yaml
- 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`. Шаблон автоматически определяет роль ноды:
```yaml
# master01 получает:
cluster-init: true
# worker01 и rpi01 получают:
server: "https://192.168.1.10:6443"
```
### kube-vip
Создаёт виртуальный IP через ARP (L2). VIP анонсируется всем устройствам в локальной сети. При отказе мастера VIP автоматически мигрирует на другую ноду в течение нескольких секунд.
Требования к VIP:
- В той же подсети что серверы
- Не занят другим устройством
- Не выдаётся DHCP сервером
**Автоопределение интерфейса:** Ansible определяет сетевой интерфейс каждой master-ноды через `ansible_default_ipv4.interface`. На Ubuntu это может быть `enp3s0`, на Raspberry Pi — `end0`, на VM — `eth0`. Принудительное задание: `kube_vip_interface: "eth0"` в `group_vars/all/main.yml`.
### NFS + CSI
NFS сервер разворачивается на master01. StorageClass автоматически именуется `nfs-<hostname NFS сервера>` — например, `nfs-master01`. Это позволяет сразу понять на какой сервер указывает StorageClass при `kubectl get sc`. Если NFS вынесен на отдельный хост с hostname `storage01`, StorageClass будет `nfs-storage01`.
Каждый PVC создаёт отдельную папку внутри `/srv/nfs/k8s`:
```
/srv/nfs/k8s/
├── default/ ← namespace
│ └── my-pvc/ ← имя PVC
│ └── pvc-xxx-yyy-zzz/ ← имя PV
│ └── данные...
```
NFS можно вынести на отдельный хост — задай в `inventory/hosts.ini`:
```ini
[nfs_server]
nfshost ansible_host=192.168.1.20 ansible_user=ubuntu
```
И в `group_vars/all/main.yml`:
```yaml
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-архитектуру и:
1. Включает `memory cgroup` в `/boot/cmdline.txt` (или `/boot/firmware/cmdline.txt` для Bookworm) и перезагружает RPi
2. Применяет пониженные kubelet-резервы (`cpu=50m, memory=128Mi`)
3. Настраивает пороги garbage collection образов (85%/80%)
4. Применяет taint `node-type=raspberry-pi:NoSchedule` — поды не планируются если нет явного toleration
**Рекомендуемая ОС:** Raspberry Pi OS Lite **64-bit (Bookworm)** для RPi 4/5.
Для деплоя **на** RPi добавь toleration в манифест:
```yaml
tolerations:
- key: "node-type"
operator: "Equal"
value: "raspberry-pi"
effect: "NoSchedule"
```
Чтобы снять ограничение и использовать RPi как обычную ноду — очисти `host_vars/rpi01/main.yml`:
```yaml
k3s_node_taints: []
```
---
## Docker образ
Образ собирается один раз через `make build` и содержит всё необходимое — как для управления кластером, так и для Molecule-тестирования:
```
FROM python:3.12-slim-bookworm
├── openssh-client, curl, jq, git
├── docker-ce-cli (Docker CLI для Molecule)
├── ansible-core 2.16, ansible 9.x
├── kubernetes, openshift Python пакеты
├── molecule >= 6.0 + molecule-plugins[docker]
├── yamllint, ansible-lint
├── Helm 3.14.4
├── kubectl v1.29.3
└── Ansible Collections:
├── community.general >= 8.0
├── ansible.posix >= 1.5
└── kubernetes.core >= 3.0
```
При обычном запуске (`make install`, `make ping`):
| Путь в контейнере | Источник | Назначение |
|---|---|---|
| `/ansible` | `$(PWD)` | Весь проект |
| `/root/.ssh` | `~/.ssh` (read-only) | SSH ключи |
При Molecule-запуске (`make molecule-*`) дополнительно:
| Путь в контейнере | Источник | Назначение |
|---|---|---|
| `/var/run/docker.sock` | хост | Docker socket для создания тестовых контейнеров |
---
## Обновление K3S
```bash
make upgrade VERSION=v1.30.0+k3s1
```
Происходит по очереди (`serial: 1` в `upgrade.yml`):
1. Drain ноды → вытеснить поды
2. Обновить K3S бинарник
3. Перезапустить сервис
4. Дождаться `Ready`
5. Uncordon → восстановить планирование
6. Перейти к следующей ноде
HA-режим гарантирует доступность кластера во время обновления: пока одна нода обновляется, остальные две обслуживают трафик.
---
## Диагностика
```bash
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
```bash
# Статус участников 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/kubernetes/k3s/server/tls/etcd/server-ca.crt \
--cert=/var/lib/kubernetes/k3s/server/tls/etcd/server-client.crt \
--key=/var/lib/kubernetes/k3s/server/tls/etcd/server-client.key \
member list
```
---
## Примеры манифестов
### Приложение с Ingress и NFS-хранилищем
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
accessModes: [ReadWriteMany]
storageClassName: nfs-master01 # или имя твоего NFS сервера
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
```yaml
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)
```yaml
spec:
template:
spec:
nodeSelector:
node-type: x86_64
```
### Приложение с доступом к Grafana/Prometheus (ServiceMonitor)
```yaml
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` и исправь указанные файлы |
| etcd restore: снимок не найден | Запусти `make etcd-list-snapshots` — снимок должен быть на первом мастере |
| add-node: нода не присоединяется | Убедись что kube-vip работает: `kubectl -n kube-system get pods \| grep kube-vip` |
| remove-node: drain завис | Поды с PodDisruptionBudget могут блокировать drain. Проверь PDB: `kubectl get pdb -A` |
| cert-manager: сертификат в Pending | `kubectl describe certificate <name>` — проверь условия. При letsencrypt нужен публичный домен и ingress |
### Отладка с подробным выводом
```bash
ANSIBLE_VERBOSITY=2 make install # основной вывод
ANSIBLE_VERBOSITY=4 make install # максимальный вывод (SSH, модули)
ANSIBLE_VERBOSITY=2 ANSIBLE_TAGS=k3s make install # подробно только для k3s
```
### Сброс и повторная установка
```bash
# Удалить весь стек (с подтверждением)
make uninstall
# Затем установить заново
make install
```