commit 095b276cb35198f7d288449e2b6b168c9c09ed56 Author: Sergey Antropoff Date: Fri Apr 17 08:37:27 2026 +0300 first commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d91d9a3 --- /dev/null +++ b/.env.example @@ -0,0 +1,35 @@ +# .env — переменные для Docker Ansible Runner +# Скопируй в .env и заполни своими значениями: +# cp .env.example .env +# +# ВАЖНО: .env добавлен в .gitignore — не коммить его! + +# ─── Обязательные ──────────────────────────────────────────────────────────── + +# Пароль от Ansible Vault (vault encrypt_string ...) +VAULT_PASSWORD=замени-на-свой-пароль-vault + +# ─── SSH ────────────────────────────────────────────────────────────────────── + +# Путь к папке с SSH ключами (монтируется в контейнер read-only) +SSH_KEY_PATH=~/.ssh + +# ─── Опциональные ──────────────────────────────────────────────────────────── + +# Уровень verbose для Ansible (0 = обычный, 1 = -v, 2 = -vv, ...) +ANSIBLE_VERBOSITY=0 + +# Запустить только определённые теги (пусто = все) +# Пример: ANSIBLE_TAGS=k3s,kube_vip +ANSIBLE_TAGS= + +# Пропустить теги +ANSIBLE_SKIP_TAGS= + +# Дополнительные переменные ansible (-e) +# Пример: EXTRA_VARS=k3s_version=v1.30.0+k3s1 +EXTRA_VARS= + +# Версия K3S для команды upgrade +# Пример: VERSION=v1.30.0+k3s1 +VERSION= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d311d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Секреты — НИКОГДА не коммитить! +.env +.vault_pass +group_vars/all/vault.yml +host_vars/*/vault.yml +*.retry + +# Kubeconfig — содержит токены доступа к кластеру +kubeconfig +*.kubeconfig + +# SSH ключи +*.pem +*.key +id_rsa +id_ed25519 +id_ecdsa + +# Docker артефакты +.docker/ +.claude/ + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +venv/ + +# Редакторы +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store + +# Временные файлы +/tmp/ +*.tmp +*.bak diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..cab88a9 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,27 @@ +--- +extends: default + +rules: + line-length: + max: 160 + level: warning + + truthy: + allowed-values: ['true', 'false', 'yes', 'no'] + check-keys: false + + comments: + min-spaces-from-content: 1 + + braces: + min-spaces-inside: 0 + max-spaces-inside: 1 + + brackets: + min-spaces-inside: 0 + max-spaces-inside: 0 + +ignore: | + .git/ + kubeconfig + roles/*/molecule/*/molecule.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f00318a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,76 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Ansible Runner — Docker образ для запуска K3S плейбуков +# Содержит: Ansible, Helm, kubectl, все нужные коллекции +# ───────────────────────────────────────────────────────────────────────────── +FROM python:3.12-slim-bookworm + +LABEL maintainer="your-name" +LABEL description="Ansible runner for K3S stack deployment" + +# ── Системные зависимости ───────────────────────────────────────────────────── +RUN apt-get update && apt-get install -y --no-install-recommends \ + # SSH + openssh-client \ + sshpass \ + # Сетевые утилиты + curl \ + wget \ + dnsutils \ + iputils-ping \ + # Утилиты + git \ + jq \ + rsync \ + unzip \ + gnupg \ + ca-certificates \ + # Для сборки некоторых Python пакетов + gcc \ + libffi-dev \ + libssl-dev \ + && rm -rf /var/lib/apt/lists/* + +# ── Python зависимости (Ansible + плагины) ──────────────────────────────────── +COPY requirements-python.txt /tmp/requirements-python.txt +RUN pip install --no-cache-dir -r /tmp/requirements-python.txt + +# ── Helm ────────────────────────────────────────────────────────────────────── +ARG HELM_VERSION=3.14.4 +RUN curl -fsSL https://get.helm.sh/helm-v${HELM_VERSION}-linux-amd64.tar.gz \ + | tar -xz --strip-components=1 -C /usr/local/bin linux-amd64/helm \ + && chmod +x /usr/local/bin/helm \ + && helm version --short + +# ── kubectl ─────────────────────────────────────────────────────────────────── +ARG KUBECTL_VERSION=v1.29.3 +RUN curl -fsSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" \ + -o /usr/local/bin/kubectl \ + && chmod +x /usr/local/bin/kubectl \ + && kubectl version --client --short 2>/dev/null || true + +# ── Ansible Galaxy коллекции ────────────────────────────────────────────────── +COPY requirements.yml /tmp/requirements.yml +RUN ansible-galaxy collection install -r /tmp/requirements.yml \ + --collections-path /usr/share/ansible/collections \ + && echo "Collections installed:" \ + && ansible-galaxy collection list + +# ── Рабочая директория ──────────────────────────────────────────────────────── +WORKDIR /ansible + +# ── Конфигурация Ansible ────────────────────────────────────────────────────── +ENV ANSIBLE_COLLECTIONS_PATHS=/usr/share/ansible/collections +ENV ANSIBLE_HOST_KEY_CHECKING=False +ENV ANSIBLE_STDOUT_CALLBACK=yaml +ENV ANSIBLE_FORCE_COLOR=1 +ENV PYTHONUNBUFFERED=1 + +# SSH agent forwarding директория +RUN mkdir -p /root/.ssh && chmod 700 /root/.ssh + +# ── Entrypoint ──────────────────────────────────────────────────────────────── +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["--help"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7684b0f --- /dev/null +++ b/Makefile @@ -0,0 +1,326 @@ +# ═══════════════════════════════════════════════════════════════════════════════ +# K3S Ansible Stack — Makefile +# Всё запускается через Docker — Ansible устанавливать не нужно! +# +# Требования: docker, make +# Начало работы: make setup → отредактируй .env и inventory → make install +# ═══════════════════════════════════════════════════════════════════════════════ + +# ── Конфигурация ────────────────────────────────────────────────────────────── +IMAGE_NAME := k3s-ansible +CONTAINER_NAME := k3s-ansible-runner + +# Загружаем .env если существует +-include .env +export + +# Цвета терминала +CYAN := \033[0;36m +GREEN := \033[0;32m +YELLOW := \033[1;33m +RED := \033[0;31m +BOLD := \033[1m +NC := \033[0m + +# ── Базовая команда запуска контейнера ──────────────────────────────────────── +DOCKER_RUN := docker run --rm -it \ + --name $(CONTAINER_NAME) \ + --network host \ + -v $(PWD):/ansible \ + -v $(or $(SSH_KEY_PATH),$(HOME)/.ssh):/root/.ssh:ro \ + -e VAULT_PASSWORD="$(VAULT_PASSWORD)" \ + -e ANSIBLE_VERBOSITY="$(ANSIBLE_VERBOSITY)" \ + -e ANSIBLE_TAGS="$(ANSIBLE_TAGS)" \ + -e ANSIBLE_SKIP_TAGS="$(ANSIBLE_SKIP_TAGS)" \ + -e EXTRA_VARS="$(EXTRA_VARS)" \ + -e VERSION="$(VERSION)" \ + -e ANSIBLE_FORCE_COLOR=1 \ + $(IMAGE_NAME) + +.PHONY: help setup build rebuild \ + install install-k3s install-cni install-kubevip install-nfs install-ingress \ + install-istio install-monitoring \ + upgrade uninstall health verify ping \ + shell lint check \ + molecule-k3s molecule-prometheus molecule-istio molecule-all molecule-lint \ + vault-create vault-edit vault-view vault-encrypt-string \ + clean clean-all \ + _check_env _check_image _check_molecule + +# ── DEFAULT ─────────────────────────────────────────────────────────────────── +.DEFAULT_GOAL := help + +help: ## Показать эту справку + @echo "" + @printf "$(CYAN)$(BOLD)K3S Ansible Stack — управление через Docker$(NC)\n" + @printf "$(CYAN)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$(NC)\n" + @echo "" + @printf "$(BOLD)Первый запуск:$(NC)\n" + @echo " 1. make setup — создать .env из шаблона" + @echo " 2. Отредактируй .env и inventory/hosts.ini" + @echo " 3. make build — собрать Docker образ" + @echo " 4. make vault-create — создать vault с токеном" + @echo " 5. make ping — проверить SSH" + @echo " 6. make install — развернуть полный стек" + @echo "" + @printf "$(BOLD)Все команды:$(NC)\n" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ + | grep -v '^_' \ + | awk 'BEGIN {FS = ":.*?## "}; {printf " $(CYAN)%-26s$(NC) %s\n", $$1, $$2}' + @echo "" + @printf "$(BOLD)Переменные (передаются в командной строке):$(NC)\n" + @printf " $(CYAN)VERSION$(NC)=v1.30.0+k3s1 версия K3S для upgrade\n" + @printf " $(CYAN)ANSIBLE_VERBOSITY$(NC)=2 уровень debug вывода (0-4)\n" + @printf " $(CYAN)ANSIBLE_TAGS$(NC)=k3s,kube_vip запустить только теги\n" + @echo "" + +# ═══════════════════════════════════════════════════════════════════════════════ +# НАСТРОЙКА +# ═══════════════════════════════════════════════════════════════════════════════ + +setup: ## Первоначальная настройка: создать .env из шаблона + @if [ -f .env ]; then \ + printf "$(YELLOW)⚠ .env уже существует, пропускаю$(NC)\n"; \ + else \ + cp .env.example .env; \ + printf "$(GREEN)✓ Создан .env$(NC)\n"; \ + fi + @echo "" + @printf "$(BOLD)Следующие шаги:$(NC)\n" + @echo " 1. nano .env — задай VAULT_PASSWORD" + @echo " 2. nano inventory/hosts.ini — укажи IP серверов" + @echo " 3. nano group_vars/all/main.yml — задай kube_vip_address и интерфейс" + @echo " 4. make build — собери Docker образ" + @echo " 5. make vault-create — создай vault" + @echo " 6. make ping — проверь SSH" + @echo " 7. make install — разверни стек" + +# ═══════════════════════════════════════════════════════════════════════════════ +# DOCKER ОБРАЗ +# ═══════════════════════════════════════════════════════════════════════════════ + +build: ## Собрать Docker образ ansible-runner + @printf "$(CYAN)Сборка образа $(IMAGE_NAME)...$(NC)\n" + docker build \ + --build-arg HELM_VERSION=3.14.4 \ + --build-arg KUBECTL_VERSION=v1.29.3 \ + -t $(IMAGE_NAME):latest \ + -f Dockerfile . + @printf "$(GREEN)✓ Образ $(IMAGE_NAME):latest собран$(NC)\n" + +rebuild: ## Пересобрать образ без кэша + @printf "$(CYAN)Пересборка без кэша...$(NC)\n" + docker build --no-cache \ + --build-arg HELM_VERSION=3.14.4 \ + --build-arg KUBECTL_VERSION=v1.29.3 \ + -t $(IMAGE_NAME):latest \ + -f Dockerfile . + @printf "$(GREEN)✓ Готово$(NC)\n" + +# ═══════════════════════════════════════════════════════════════════════════════ +# ПРОВЕРКИ +# ═══════════════════════════════════════════════════════════════════════════════ + +ping: _check_env _check_image ## Проверить SSH доступность всех нод + @printf "$(CYAN)Проверка SSH...$(NC)\n" + $(DOCKER_RUN) ping + +check: _check_env _check_image ## Dry-run: проверить плейбук без изменений + @printf "$(CYAN)Dry-run...$(NC)\n" + $(DOCKER_RUN) ansible-playbook site.yml --check --diff + +lint: _check_image ## Проверить синтаксис всех плейбуков + @printf "$(CYAN)Проверка синтаксиса...$(NC)\n" + $(DOCKER_RUN) ansible-playbook site.yml --syntax-check + @printf "$(GREEN)✓ Синтаксис корректен$(NC)\n" + +# ═══════════════════════════════════════════════════════════════════════════════ +# УСТАНОВКА +# ═══════════════════════════════════════════════════════════════════════════════ + +install: _check_env _check_image ## Развернуть полный стек (K3S + kube-vip + NFS + ingress) + @printf "$(CYAN)$(BOLD)Разворачиваю полный K3S стек...$(NC)\n" + $(DOCKER_RUN) install + +install-k3s: _check_env _check_image ## Установить только K3S кластер + @printf "$(CYAN)Устанавливаю K3S...$(NC)\n" + $(DOCKER_RUN) install-k3s + +install-cni: _check_env _check_image ## Установить CNI плагин (задай K3S_CNI=calico|cilium) + @printf "$(CYAN)Устанавливаю CNI ($(or $(K3S_CNI),flannel))...$(NC)\n" + $(DOCKER_RUN) ansible-playbook site.yml --tags cni $(if $(K3S_CNI),-e k3s_cni=$(K3S_CNI),) + +install-kubevip: _check_env _check_image ## Установить только kube-vip + @printf "$(CYAN)Устанавливаю kube-vip...$(NC)\n" + $(DOCKER_RUN) install-kubevip + +install-nfs: _check_env _check_image ## Установить NFS сервер + CSI Driver + @printf "$(CYAN)Устанавливаю NFS + CSI...$(NC)\n" + $(DOCKER_RUN) install-nfs + +install-ingress: _check_env _check_image ## Установить ingress-nginx + @printf "$(CYAN)Устанавливаю ingress-nginx...$(NC)\n" + $(DOCKER_RUN) install-ingress + +install-istio: _check_env _check_image ## Установить Istio + Kiali (нужен istio_enabled: true в vars) + @printf "$(CYAN)Устанавливаю Istio + Kiali...$(NC)\n" + $(DOCKER_RUN) ansible-playbook site.yml --tags istio -e istio_enabled=true + +install-monitoring: _check_env _check_image ## Установить Prometheus + Grafana (нужен prometheus_stack_enabled: true) + @printf "$(CYAN)Устанавливаю kube-prometheus-stack...$(NC)\n" + $(DOCKER_RUN) ansible-playbook site.yml --tags monitoring -e prometheus_stack_enabled=true + +# ═══════════════════════════════════════════════════════════════════════════════ +# ОБНОВЛЕНИЕ +# ═══════════════════════════════════════════════════════════════════════════════ + +upgrade: _check_env _check_image ## Обновить K3S (make upgrade VERSION=v1.30.0+k3s1) + @if [ -z "$(VERSION)" ]; then \ + printf "$(RED)✗ Укажи версию: make upgrade VERSION=v1.30.0+k3s1$(NC)\n"; \ + exit 1; \ + fi + @printf "$(CYAN)Обновляю K3S до $(VERSION)...$(NC)\n" + $(DOCKER_RUN) upgrade + +# ═══════════════════════════════════════════════════════════════════════════════ +# ДИАГНОСТИКА +# ═══════════════════════════════════════════════════════════════════════════════ + +health: _check_env _check_image ## Диагностика кластера (сервисы, поды, диск, память) + @printf "$(CYAN)Диагностика...$(NC)\n" + $(DOCKER_RUN) health + +verify: _check_env _check_image ## Проверить весь стек (nodes/pods/svc/sc/ingress) + @printf "$(CYAN)Проверка стека...$(NC)\n" + $(DOCKER_RUN) verify + +# ═══════════════════════════════════════════════════════════════════════════════ +# УДАЛЕНИЕ +# ═══════════════════════════════════════════════════════════════════════════════ + +uninstall: _check_env _check_image ## ВНИМАНИЕ: удалить весь стек и данные + @printf "$(RED)$(BOLD)ВНИМАНИЕ: Удаление всего стека — данные будут потеряны!$(NC)\n" + @printf "$(YELLOW)Введи 'yes' для подтверждения: $(NC)"; \ + read CONFIRM && [ "$$CONFIRM" = "yes" ] || (printf "Отменено.\n" && exit 1) + $(DOCKER_RUN) uninstall + +# ═══════════════════════════════════════════════════════════════════════════════ +# ANSIBLE VAULT +# ═══════════════════════════════════════════════════════════════════════════════ + +vault-create: _check_image ## Создать зашифрованный vault (group_vars/all/vault.yml) + @printf "$(CYAN)Создание vault...$(NC)\n" + docker run --rm -it \ + -v $(PWD):/ansible \ + $(IMAGE_NAME) \ + ansible vault create group_vars/all/vault.yml + @printf "$(GREEN)✓ Vault создан$(NC)\n" + +vault-edit: _check_image ## Редактировать vault + docker run --rm -it \ + -v $(PWD):/ansible \ + -e VAULT_PASSWORD="$(VAULT_PASSWORD)" \ + $(IMAGE_NAME) \ + ansible vault edit group_vars/all/vault.yml + +vault-view: _check_image ## Просмотреть vault (расшифрованный вывод) + docker run --rm -it \ + -v $(PWD):/ansible \ + -e VAULT_PASSWORD="$(VAULT_PASSWORD)" \ + $(IMAGE_NAME) \ + ansible vault view group_vars/all/vault.yml + +vault-encrypt-string: _check_image ## Зашифровать строку: make vault-encrypt-string STR=... NAME=... + @if [ -z "$(STR)" ]; then \ + printf "$(RED)✗ Нужна строка: make vault-encrypt-string STR=мой-токен NAME=vault_k3s_token$(NC)\n"; \ + exit 1; \ + fi + docker run --rm -it \ + -v $(PWD):/ansible \ + -e VAULT_PASSWORD="$(VAULT_PASSWORD)" \ + $(IMAGE_NAME) \ + ansible vault encrypt_string '$(STR)' --name '$(or $(NAME),encrypted_var)' + +# ═══════════════════════════════════════════════════════════════════════════════ +# MOLECULE — ТЕСТИРОВАНИЕ РОЛЕЙ +# Требования: pip install -r requirements-python.txt (molecule, molecule-plugins[docker]) +# ═══════════════════════════════════════════════════════════════════════════════ + +molecule-k3s: _check_molecule ## Тест роли k3s (Docker, ~5 мин) + @printf "$(CYAN)Тестирую роль k3s...$(NC)\n" + cd roles/k3s && molecule test + @printf "$(GREEN)✓ k3s role: OK$(NC)\n" + +molecule-prometheus: _check_molecule ## Тест роли prometheus-stack (шаблоны + PVC) + @printf "$(CYAN)Тестирую роль prometheus-stack...$(NC)\n" + cd roles/prometheus-stack && molecule test + @printf "$(GREEN)✓ prometheus-stack role: OK$(NC)\n" + +molecule-istio: _check_molecule ## Тест роли istio + kiali (шаблоны) + @printf "$(CYAN)Тестирую роль istio...$(NC)\n" + cd roles/istio && molecule test + @printf "$(GREEN)✓ istio role: OK$(NC)\n" + +molecule-all: _check_molecule ## Запустить все Molecule тесты последовательно + @printf "$(CYAN)$(BOLD)Запуск всех Molecule тестов...$(NC)\n" + $(MAKE) molecule-k3s + $(MAKE) molecule-prometheus + $(MAKE) molecule-istio + @printf "$(GREEN)$(BOLD)✓ Все тесты прошли успешно$(NC)\n" + +molecule-lint: _check_molecule ## Только линтинг (yamllint + ansible-lint), без Docker + @printf "$(CYAN)Запуск линтинга...$(NC)\n" + yamllint . + ansible-lint + @printf "$(GREEN)✓ Линтинг прошёл$(NC)\n" + +# ═══════════════════════════════════════════════════════════════════════════════ +# ИНТЕРАКТИВНЫЙ SHELL +# ═══════════════════════════════════════════════════════════════════════════════ + +shell: _check_image ## Запустить интерактивный bash внутри контейнера + @printf "$(CYAN)Запуск shell (exit для выхода)...$(NC)\n" + docker run --rm -it \ + --name $(CONTAINER_NAME)-shell \ + --network host \ + -v $(PWD):/ansible \ + -v $(or $(SSH_KEY_PATH),$(HOME)/.ssh):/root/.ssh:ro \ + -e VAULT_PASSWORD="$(VAULT_PASSWORD)" \ + -e ANSIBLE_FORCE_COLOR=1 \ + $(IMAGE_NAME) shell + +# ═══════════════════════════════════════════════════════════════════════════════ +# ОЧИСТКА +# ═══════════════════════════════════════════════════════════════════════════════ + +clean: ## Удалить Docker образ + @printf "$(YELLOW)Удаляю образ $(IMAGE_NAME)...$(NC)\n" + docker rmi $(IMAGE_NAME):latest 2>/dev/null || printf "$(YELLOW)Образ не найден$(NC)\n" + +clean-all: clean ## Удалить образ + kubeconfig + кэш Docker + rm -f kubeconfig + docker system prune -f + @printf "$(GREEN)✓ Очистка завершена$(NC)\n" + +# ═══════════════════════════════════════════════════════════════════════════════ +# ВНУТРЕННИЕ +# ═══════════════════════════════════════════════════════════════════════════════ + +_check_env: + @if [ ! -f .env ]; then \ + printf "$(RED)✗ Файл .env не найден! Запусти: make setup$(NC)\n"; \ + exit 1; \ + fi + +_check_image: + @if ! docker image inspect $(IMAGE_NAME):latest > /dev/null 2>&1; then \ + printf "$(YELLOW)⚠ Образ не найден, собираю...$(NC)\n"; \ + $(MAKE) build; \ + fi + +_check_molecule: + @if ! python3 -c "import molecule" 2>/dev/null; then \ + printf "$(RED)✗ Molecule не установлен!$(NC)\n"; \ + printf "$(YELLOW) Запусти: pip install -r requirements-python.txt$(NC)\n"; \ + exit 1; \ + fi diff --git a/README.md b/README.md new file mode 100644 index 0000000..1175e66 --- /dev/null +++ b/README.md @@ -0,0 +1,1401 @@ +# K3S Ansible Stack + +> Полный Kubernetes стек на базе K3S с HA (High Availability), управляемый через Ansible внутри Docker-контейнера. +> Ansible устанавливать **не нужно** — всё работает через `make`. + +## Содержание + +- [Архитектура](#архитектура) +- [Требования](#требования) +- [Структура проекта](#структура-проекта) +- [Полная установка с нуля](#полная-установка-с-нуля) +- [Рабочий процесс](#рабочий-процесс) +- [Настройка кластера](#настройка-кластера) +- [Опциональные компоненты](#опциональные-компоненты) +- [Все команды 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-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+: +```bash +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 без пароля **на каждом сервере**: +```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 +│ +├── 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 на свои): + +```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 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" # узнать: ssh ubuntu@192.168.1.10 "ip -br a" +``` + +### Шаг 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 — Прогнать тесты (рекомендуется) + +Перед деплоем убедись что роли корректны: + +```bash +# Установить зависимости для Molecule (один раз) +pip install -r requirements-python.txt + +# Запустить все тесты (~10-15 минут) +make molecule-all +``` + +Если всё зелёное — можно деплоить. Если есть ошибки — смотри раздел [Тестирование через 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 → kube-vip +2. worker01: prereqs → K3S server (join) → ждёт готовности ноды +3. rpi01: prereqs + cgroups → K3S server (join) → применяет taint NoSchedule +4. NFS server на master01 +5. CSI NFS Driver на всех нодах +6. ingress-nginx через Helm +7. Финальная проверка: 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-client (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 +k3s_disable_traefik: true # ОБЯЗАТЕЛЬНО true при ingress-nginx +``` + +### kube-vip + +```yaml +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 + +```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 +``` + +### 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`. По умолчанию все отключены — базовый стек работает без них. + +--- + +### 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 и настраивается на их использование без дополнительной конфигурации. + +--- + +## Все команды 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-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) + +```bash +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 сек) +``` + +### Обновление и диагностика + +```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-контейнере, проходит набор автоматических проверок и удаляется. Реальные серверы при этом **не нужны**. + +### Установка + +```bash +# Установить зависимости (один раз) +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.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` | + +--- + +### Запуск тестов: пошагово + +#### Быстрая проверка (lint, без Docker) + +```bash +make molecule-lint +``` + +Запускает `yamllint .` и `ansible-lint` на всём проекте. Выполняется за ~30 секунд, Docker не нужен. Используй перед каждым коммитом. + +``` +Запуск линтинга... +yamllint . +ansible-lint +✓ Линтинг прошёл +``` + +#### Тест одной роли + +```bash +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 +``` + +#### Все тесты + +```bash +make molecule-all +``` + +``` +Тестирую роль k3s... +... +✓ k3s role: OK + +Тестирую роль prometheus-stack... +... +✓ prometheus-stack role: OK + +Тестирую роль istio... +... +✓ istio role: OK + +✓ Все тесты прошли успешно +``` + +--- + +### Отладка упавших тестов + +Если тест упал — контейнер удаляется автоматически. Чтобы оставить контейнер живым для ручной отладки, используй отдельные фазы: + +```bash +# Перейти в директорию роли +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` | + +#### Запуск конкретной фазы вручную + +```bash +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`** — конфигурация драйвера и платформы: +```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 сервером + +### 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`: +```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` и содержит всё необходимое для управления кластером: + +``` +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 + +```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/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-хранилищем + +```yaml +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 + +```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 `` | `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` и исправь указанные файлы | + +### Отладка с подробным выводом + +```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 +``` diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..93b3816 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,16 @@ +[defaults] +inventory = inventory/hosts.ini +roles_path = roles +host_key_checking = False +interpreter_python = auto_silent +stdout_callback = yaml +timeout = 30 + +[ssh_connection] +pipelining = True +ssh_args = -o ControlMaster=auto -o ControlPersist=60s + +[privilege_escalation] +become = True +become_method = sudo +become_ask_pass = False diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5f15204 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,60 @@ +# docker-compose.yml — Ansible Runner для K3S стека +# Использование: управляется через Makefile (make install, make health, ...) +# Прямой запуск: docker compose run --rm ansible install + +version: "3.8" + +services: + ansible: + build: + context: . + dockerfile: Dockerfile + args: + HELM_VERSION: "3.14.4" + KUBECTL_VERSION: "v1.29.3" + image: k3s-ansible:latest + container_name: k3s-ansible-runner + + # ── Volumes ────────────────────────────────────────────────────────────── + volumes: + # Весь проект — монтируется внутрь контейнера + - .:/ansible + + # SSH ключи с хост-машины (read-only) + - ${SSH_KEY_PATH:-~/.ssh}:/root/.ssh:ro + + # Vault-пароль файл (опционально) + # - ./.vault_pass:/ansible/.vault_pass:ro + + # ── Переменные окружения ────────────────────────────────────────────────── + environment: + # Пароль vault — задай в .env файле или передай через командную строку + VAULT_PASSWORD: ${VAULT_PASSWORD:-} + + # Уровень подробности вывода (0 = обычный, 1-4 = debug) + ANSIBLE_VERBOSITY: ${ANSIBLE_VERBOSITY:-0} + + # Теги для запуска (пусто = все) + ANSIBLE_TAGS: ${ANSIBLE_TAGS:-} + ANSIBLE_SKIP_TAGS: ${ANSIBLE_SKIP_TAGS:-} + + # Дополнительные переменные + EXTRA_VARS: ${EXTRA_VARS:-} + + # Версия K3S для upgrade + VERSION: ${VERSION:-} + + # Ansible настройки + ANSIBLE_FORCE_COLOR: "1" + ANSIBLE_HOST_KEY_CHECKING: "False" + ANSIBLE_STDOUT_CALLBACK: "yaml" + + # ── Рабочая директория ──────────────────────────────────────────────────── + working_dir: /ansible + + # ── Сеть: хост (нужен для SSH до серверов в локальной сети) ────────────── + network_mode: host + + # ── Интерактивный режим (нужен для --ask-vault-pass) ───────────────────── + stdin_open: true + tty: true diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..25af2b0 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,286 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# Entrypoint для Ansible Runner контейнера +# Выполняет: настройку SSH, vault-пароля, и запуск ansible-playbook +# ───────────────────────────────────────────────────────────────────────────── +set -euo pipefail + +# Цвета для вывода +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' # No Color + +log() { echo -e "${BLUE}[runner]${NC} $*"; } +ok() { echo -e "${GREEN}[runner]${NC} ✓ $*"; } +warn() { echo -e "${YELLOW}[runner]${NC} ⚠ $*"; } +err() { echo -e "${RED}[runner]${NC} ✗ $*" >&2; } + +# ── Баннер ──────────────────────────────────────────────────────────────────── +print_banner() { + echo -e "${CYAN}" + echo "╔══════════════════════════════════════════════════════╗" + echo "║ K3S Ansible Runner 🚀 ║" + echo "║ Ansible $(ansible --version | head -1 | awk '{print $3}') • Helm $(helm version --short) • kubectl $(kubectl version --client -o json 2>/dev/null | jq -r .clientVersion.gitVersion 2>/dev/null || echo 'n/a') ║" + echo "╚══════════════════════════════════════════════════════╝" + echo -e "${NC}" +} + +# ── Справка ─────────────────────────────────────────────────────────────────── +print_help() { + echo -e "${BOLD}Использование:${NC}" + echo "" + echo " Через Makefile (рекомендуется):" + echo " make install — полный стек" + echo " make install-k3s — только K3S" + echo " make install-kubevip — только kube-vip" + echo " make install-nfs — NFS сервер + CSI" + echo " make install-ingress — ingress-nginx" + echo " make health — диагностика" + echo " make upgrade VERSION=v1.30.0+k3s1" + echo "" + echo " Напрямую через docker run:" + echo " docker run --rm -it \\" + echo " -v \$(pwd):/ansible \\" + echo " -v ~/.ssh:/root/.ssh:ro \\" + echo " -e VAULT_PASSWORD=мой-пароль \\" + echo " k3s-ansible install" + echo "" + echo -e "${BOLD}Переменные окружения:${NC}" + echo " VAULT_PASSWORD — пароль Ansible Vault" + echo " VAULT_PASSWORD_FILE — путь к файлу с паролем (альтернатива)" + echo " ANSIBLE_TAGS — запустить только указанные теги" + echo " ANSIBLE_SKIP_TAGS — пропустить указанные теги" + echo " EXTRA_VARS — дополнительные переменные (-e)" + echo " ANSIBLE_VERBOSITY — уровень verbose (0-4, default: 0)" + echo "" + echo -e "${BOLD}Доступные команды:${NC}" + echo " install — полный стек (site.yml)" + echo " install-k3s — только K3S" + echo " install-kubevip — только kube-vip" + echo " install-nfs — NFS + CSI" + echo " install-ingress — ingress-nginx" + echo " upgrade — обновить K3S (нужен VERSION=)" + echo " uninstall — удалить весь стек" + echo " health — диагностика" + echo " verify — проверка стека" + echo " ping — проверить SSH до всех нод" + echo " shell — интерактивный bash" + echo " ansible-playbook — прямой вызов ansible-playbook" + echo " ansible — прямой вызов ansible" +} + +# ── Настройка SSH ───────────────────────────────────────────────────────────── +setup_ssh() { + log "Настройка SSH..." + + # Создаём директорию если не существует + mkdir -p /root/.ssh + chmod 700 /root/.ssh + + # Конфигурация SSH клиента + cat > /root/.ssh/config << 'SSHEOF' +Host * + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + ControlMaster auto + ControlPath /tmp/ssh-%r@%h:%p + ControlPersist 60s + ServerAliveInterval 30 + ServerAliveCountMax 3 + ConnectTimeout 30 +SSHEOF + chmod 600 /root/.ssh/config + + # Исправляем права на ключи если они примонтированы + if ls /root/.ssh/*.pem 2>/dev/null || ls /root/.ssh/id_* 2>/dev/null; then + chmod 600 /root/.ssh/id_* 2>/dev/null || true + chmod 600 /root/.ssh/*.pem 2>/dev/null || true + ok "SSH ключи найдены" + else + warn "SSH ключи не найдены в /root/.ssh — убедись что они примонтированы" + fi +} + +# ── Настройка Vault пароля ──────────────────────────────────────────────────── +setup_vault() { + local vault_file="/tmp/.vault_pass" + + if [[ -n "${VAULT_PASSWORD:-}" ]]; then + echo "${VAULT_PASSWORD}" > "${vault_file}" + chmod 600 "${vault_file}" + export ANSIBLE_VAULT_PASSWORD_FILE="${vault_file}" + ok "Vault пароль установлен из переменной VAULT_PASSWORD" + + elif [[ -n "${VAULT_PASSWORD_FILE:-}" ]] && [[ -f "${VAULT_PASSWORD_FILE}" ]]; then + export ANSIBLE_VAULT_PASSWORD_FILE="${VAULT_PASSWORD_FILE}" + ok "Vault пароль загружен из файла: ${VAULT_PASSWORD_FILE}" + + elif [[ -f "/ansible/.vault_pass" ]]; then + export ANSIBLE_VAULT_PASSWORD_FILE="/ansible/.vault_pass" + ok "Vault пароль найден в /ansible/.vault_pass" + + else + warn "Vault пароль не задан. Используй переменную VAULT_PASSWORD или файл .vault_pass" + warn "Если vault не используется — это нормально" + fi +} + +# ── Сборка аргументов ansible-playbook ─────────────────────────────────────── +build_ansible_args() { + local args=() + + # Файл vault-пароля + if [[ -n "${ANSIBLE_VAULT_PASSWORD_FILE:-}" ]]; then + args+=("--vault-password-file" "${ANSIBLE_VAULT_PASSWORD_FILE}") + fi + + # Уровень verbose + local verbosity="${ANSIBLE_VERBOSITY:-0}" + if [[ "${verbosity}" -gt 0 ]]; then + args+=("-$(printf 'v%.0s' $(seq 1 "${verbosity}"))") + fi + + # Теги + if [[ -n "${ANSIBLE_TAGS:-}" ]]; then + args+=("--tags" "${ANSIBLE_TAGS}") + fi + + # Пропустить теги + if [[ -n "${ANSIBLE_SKIP_TAGS:-}" ]]; then + args+=("--skip-tags" "${ANSIBLE_SKIP_TAGS}") + fi + + # Дополнительные переменные + if [[ -n "${EXTRA_VARS:-}" ]]; then + args+=("-e" "${EXTRA_VARS}") + fi + + echo "${args[@]:-}" +} + +# ── Запуск playbook ─────────────────────────────────────────────────────────── +run_playbook() { + local playbook="$1" + shift + local extra_args=("$@") + + # shellcheck disable=SC2207 + local ansible_args=($(build_ansible_args)) + + log "Запуск: ansible-playbook ${playbook} ${ansible_args[*]} ${extra_args[*]}" + echo "" + + exec ansible-playbook "${playbook}" \ + "${ansible_args[@]}" \ + "${extra_args[@]}" +} + +# ───────────────────────────────────────────────────────────────────────────── +# MAIN +# ───────────────────────────────────────────────────────────────────────────── +print_banner +setup_ssh +setup_vault + +COMMAND="${1:-help}" +shift || true + +case "${COMMAND}" in + + # ── Основные команды ─────────────────────────────────────────────────────── + install) + log "Разворачиваю полный K3S стек..." + run_playbook site.yml "$@" + ;; + + install-k3s) + log "Устанавливаю K3S cluster..." + run_playbook site.yml --tags k3s "$@" + ;; + + install-kubevip) + log "Устанавливаю kube-vip..." + run_playbook site.yml --tags kube_vip "$@" + ;; + + install-nfs) + log "Устанавливаю NFS Server + CSI Driver..." + run_playbook site.yml --tags nfs,csi_nfs "$@" + ;; + + install-ingress) + log "Устанавливаю ingress-nginx..." + run_playbook site.yml --tags ingress_nginx "$@" + ;; + + upgrade) + if [[ -z "${VERSION:-}" ]]; then + err "Нужна переменная VERSION. Пример: make upgrade VERSION=v1.30.0+k3s1" + exit 1 + fi + log "Обновляю K3S до ${VERSION}..." + run_playbook upgrade.yml -e "k3s_version=${VERSION}" "$@" + ;; + + uninstall) + warn "Удаление всего стека! Данные будут потеряны." + run_playbook uninstall.yml -e "confirm_uninstall=yes" "$@" + ;; + + health) + log "Диагностика кластера..." + run_playbook healthcheck.yml "$@" + ;; + + verify) + log "Проверка полного стека..." + run_playbook site.yml --tags verify "$@" + ;; + + ping) + log "Проверяю SSH доступность всех нод..." + # shellcheck disable=SC2207 + local ansible_args=($(build_ansible_args)) + exec ansible all -m ping "${ansible_args[@]}" "$@" + ;; + + # ── Прямые вызовы ───────────────────────────────────────────────────────── + ansible-playbook) + exec ansible-playbook "$@" + ;; + + ansible) + exec ansible "$@" + ;; + + helm) + exec helm "$@" + ;; + + kubectl) + exec kubectl "$@" + ;; + + # ── Shell ───────────────────────────────────────────────────────────────── + shell|bash|sh) + log "Запуск интерактивного shell..." + exec /bin/bash "$@" + ;; + + # ── Помощь ──────────────────────────────────────────────────────────────── + help|--help|-h) + print_help + ;; + + *) + err "Неизвестная команда: ${COMMAND}" + echo "" + print_help + exit 1 + ;; + +esac diff --git a/group_vars/all/main.yml b/group_vars/all/main.yml new file mode 100644 index 0000000..3058049 --- /dev/null +++ b/group_vars/all/main.yml @@ -0,0 +1,125 @@ +--- +# ─── K3S ────────────────────────────────────────────────────────────────────── +k3s_version: "v1.29.3+k3s1" + +# Токен из vault (создай group_vars/all/vault.yml) +k3s_token: "{{ vault_k3s_token }}" + +k3s_install_dir: /usr/local/bin +k3s_config_dir: /etc/rancher/k3s +k3s_data_dir: /var/lib/rancher/k3s + +k3s_cluster_cidr: "10.42.0.0/16" +k3s_service_cidr: "10.43.0.0/16" +k3s_cluster_dns: "10.43.0.10" + +k3s_flannel_backend: "vxlan" + +# CNI плагин: flannel (встроен в k3s) | calico | cilium +# При calico/cilium — Flannel автоматически отключается в конфиге k3s +k3s_cni: "flannel" + +# Traefik ДОЛЖЕН быть отключён если используется ingress-nginx +k3s_disable_traefik: true +k3s_disable_servicelb: false +k3s_disable_local_storage: false + +k3s_node_labels: [] +k3s_node_taints: [] +k3s_extra_server_args: "" +k3s_extra_agent_args: "" +k3s_become: true + +# ─── kube-vip ───────────────────────────────────────────────────────────────── +# ОБЯЗАТЕЛЬНО: задай свободный IP из твоей подсети, не занятый DHCP! +kube_vip_address: "192.168.1.100" + +# Сетевой интерфейс на master-ноде. +# Оставь пустым — Ansible автоопределит через ansible_default_ipv4.interface. +# Переопредели если нужно принудительно: kube_vip_interface: "eth0" +kube_vip_interface: "" + +kube_vip_version: "v0.8.3" +kube_vip_mode: "arp" # arp (L2) | bgp (L3) +kube_vip_services_enable: true # также обрабатывает LoadBalancer Services + +# ─── NFS Server ─────────────────────────────────────────────────────────────── +nfs_exports: + - path: /srv/nfs/k8s + options: "*(rw,sync,no_subtree_check,no_root_squash)" + +nfs_allowed_network: "192.168.1.0/24" +nfs_create_export_dirs: true +nfs_export_dir_mode: "0777" +nfs_export_dir_owner: "nobody" +nfs_export_dir_group: "nogroup" + +# ─── CSI NFS Driver ─────────────────────────────────────────────────────────── +csi_nfs_version: "v4.8.0" +csi_nfs_namespace: "kube-system" + +# IP NFS сервера — по умолчанию берётся master нода +# Если NFS на отдельном хосте: csi_nfs_server: "192.168.1.20" +csi_nfs_server: "{{ hostvars[groups['k3s_master'][0]]['ansible_host'] }}" +csi_nfs_share: "/srv/nfs/k8s" + +# StorageClass именуется по hostname NFS сервера: nfs-master01, nfs-storage01, … +# Переопредели только если нужно другое имя. +csi_nfs_storageclass_default: true +csi_nfs_reclaim_policy: "Delete" # Delete | Retain +csi_nfs_on_delete: "delete" # delete | retain | archive +csi_nfs_install_client: true + +# ─── ingress-nginx ──────────────────────────────────────────────────────────── +ingress_nginx_version: "4.10.1" +ingress_nginx_namespace: "ingress-nginx" + +# LoadBalancer получит IP от kube-vip +ingress_nginx_service_type: "LoadBalancer" + +# Конкретный IP из пула kube-vip (оставь "" для автоматического) +ingress_nginx_load_balancer_ip: "" + +ingress_nginx_replica_count: 1 +ingress_nginx_use_daemonset: false +ingress_nginx_metrics_enabled: false +ingress_nginx_class_name: "nginx" +ingress_nginx_set_default_class: true + +ingress_nginx_resources: + requests: + cpu: 100m + memory: 90Mi + limits: + cpu: 500m + memory: 256Mi + +ingress_nginx_extra_args: {} + +# ─── Istio (Service Mesh) ────────────────────────────────────────────────────── +# Установка по желанию: поменяй на true чтобы включить +istio_enabled: false +# istio_version: "1.22.2" +# istio_mtls_mode: "STRICT" # STRICT | PERMISSIVE | DISABLE +# istio_install_gateway: true + +# Kiali (UI для Istio) — требует istio_enabled: true +# Токен задаётся в vault.yml: +# vault_kiali_token: "" # заполни после первой установки (токен выведет Ansible) +kiali_enabled: false + +# ─── kube-prometheus-stack (Prometheus + Grafana + Alertmanager) ─────────────── +# Установка по желанию: поменяй на true чтобы включить +prometheus_stack_enabled: false +# prometheus_stack_version: "60.3.0" +# prometheus_retention_days: 7 +# prometheus_storage_size: "10Gi" + +# Grafana: логин и пароль — задай в vault.yml: +# vault_grafana_user: "admin" +# vault_grafana_password: "ваш-пароль" +# grafana_admin_user: "{{ vault_grafana_user | default('admin') }}" +# prometheus_grafana_admin_password: "{{ vault_grafana_password | default('admin') }}" + +# prometheus_grafana_ingress_enabled: false +# prometheus_grafana_ingress_host: "grafana.local" diff --git a/group_vars/all/vault.yml.example b/group_vars/all/vault.yml.example new file mode 100644 index 0000000..c9aef8a --- /dev/null +++ b/group_vars/all/vault.yml.example @@ -0,0 +1,14 @@ +--- +# Секретные переменные — шифруй через ansible-vault! +# +# Создание зашифрованного файла: +# ansible-vault create group_vars/all/vault.yml +# +# Редактирование: +# ansible-vault edit group_vars/all/vault.yml +# +# Запуск плейбука с vault: +# ansible-playbook site.yml --ask-vault-pass +# ansible-playbook site.yml --vault-password-file ~/.vault_pass + +vault_k3s_token: "ЗАМЕНИ_НА_НАДЁЖНЫЙ_СЕКРЕТНЫЙ_ТОКЕН_МИН_32_СИМВОЛА" diff --git a/healthcheck.yml b/healthcheck.yml new file mode 100644 index 0000000..2800dc1 --- /dev/null +++ b/healthcheck.yml @@ -0,0 +1,13 @@ +--- +# Плейбук диагностики кластера +# Запуск: ansible-playbook healthcheck.yml + +- name: K3S Cluster Health Check + hosts: k3s_cluster + gather_facts: true + become: true + tasks: + - name: Run health checks + ansible.builtin.include_role: + name: k3s + tasks_from: healthcheck diff --git a/host_vars/master01/main.yml b/host_vars/master01/main.yml new file mode 100644 index 0000000..ec26d07 --- /dev/null +++ b/host_vars/master01/main.yml @@ -0,0 +1,16 @@ +--- +# Индивидуальные настройки для master01 (x64) + +# Дополнительные labels для этой ноды +k3s_node_labels: + - "node-role=master" + - "node-type=x86_64" + - "disk-type=ssd" + +# Отключить встроенный балансировщик (если используешь MetalLB) +# k3s_disable_servicelb: true + +# Дополнительные аргументы K3S сервера для этой ноды +k3s_extra_server_args: | + # Задержка вытеснения подов при нехватке памяти + kube-controller-manager-arg: "node-monitor-grace-period=20s" diff --git a/host_vars/rpi01/main.yml b/host_vars/rpi01/main.yml new file mode 100644 index 0000000..663366b --- /dev/null +++ b/host_vars/rpi01/main.yml @@ -0,0 +1,20 @@ +--- +# Индивидуальные настройки для Raspberry Pi (ARM) — мастер без планирования рабочих нагрузок + +k3s_node_labels: + - "node-type=raspberry-pi" + - "arch=arm64" + - "workload=edge" + +# Taint: мастер-нода etcd, обычные поды сюда не попадают +k3s_node_taints: + - "node-type=raspberry-pi:NoSchedule" + +# На RPi меньше ресурсов — резервируем меньше +k3s_extra_server_args: | + kubelet-arg: + - "kube-reserved=cpu=50m,memory=128Mi" + - "system-reserved=cpu=50m,memory=128Mi" + - "eviction-hard=memory.available<100Mi" + - "image-gc-high-threshold=85" + - "image-gc-low-threshold=80" diff --git a/host_vars/worker01/main.yml b/host_vars/worker01/main.yml new file mode 100644 index 0000000..621bf35 --- /dev/null +++ b/host_vars/worker01/main.yml @@ -0,0 +1,13 @@ +--- +# Индивидуальные настройки для worker01 (x64) — теперь полноценный мастер + +k3s_node_labels: + - "node-type=x86_64" + - "workload=general" + - "disk-type=hdd" + +k3s_extra_server_args: | + kubelet-arg: + - "kube-reserved=cpu=100m,memory=256Mi" + - "system-reserved=cpu=100m,memory=256Mi" + - "eviction-hard=memory.available<200Mi" diff --git a/inventory/hosts.ini b/inventory/hosts.ini new file mode 100644 index 0000000..846300d --- /dev/null +++ b/inventory/hosts.ini @@ -0,0 +1,17 @@ +[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_rsa + +# NFS сервер — по умолчанию master, но можно вынести на отдельную машину +[nfs_server] +master01 + +# Если NFS на отдельной машине — закомментируй строку выше и раскомментируй: +# nfshost ansible_host=192.168.1.20 ansible_user=ubuntu diff --git a/requirements-python.txt b/requirements-python.txt new file mode 100644 index 0000000..2c39c5e --- /dev/null +++ b/requirements-python.txt @@ -0,0 +1,28 @@ +# Ansible core + плагины для K3S стека +ansible-core>=2.16,<2.18 +ansible>=9.0.0 + +# Kubernetes / Helm модули +kubernetes>=28.1.0 +openshift>=0.13.2 + +# Утилиты +jinja2>=3.1.0 +pyyaml>=6.0 +netaddr>=0.10.0 # для ansible.utils.ipaddr +dnspython>=2.4.0 # для DNS lookup плагинов +cryptography>=41.0.0 # для Ansible Vault + +# Docker API (для Molecule docker driver + основной runner) +docker>=7.0.0 + +# ─── Molecule (тестирование ролей) ──────────────────────────────────────────── +molecule>=6.0.3 +molecule-plugins[docker]>=23.5.3 + +# Линтинг +ansible-lint>=24.2.0 +yamllint>=1.35.0 + +# Цветной вывод +rich>=13.0.0 diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..ad905d1 --- /dev/null +++ b/requirements.yml @@ -0,0 +1,8 @@ +--- +collections: + - name: community.general + version: ">=8.0.0" + - name: ansible.posix + version: ">=1.5.0" + - name: kubernetes.core + version: ">=3.0.0" diff --git a/roles/cni/defaults/main.yml b/roles/cni/defaults/main.yml new file mode 100644 index 0000000..a644471 --- /dev/null +++ b/roles/cni/defaults/main.yml @@ -0,0 +1,32 @@ +--- +# CNI плагин выбирается через k3s_cni в group_vars/all/main.yml: +# flannel (по умолчанию, встроен в k3s) +# calico — Tigera operator +# cilium — eBPF, устанавливается через Helm + +# ─── Calico ─────────────────────────────────────────────────────────────────── +calico_version: "v3.28.0" +calico_operator_url: "https://raw.githubusercontent.com/projectcalico/calico/{{ calico_version }}/manifests/tigera-operator.yaml" +calico_namespace: "calico-system" + +# CIDR должен совпадать с k3s_cluster_cidr +calico_pod_cidr: "{{ k3s_cluster_cidr | default('10.42.0.0/16') }}" + +# Encapsulation: VXLAN | IPIP | None +calico_encapsulation: "VXLAN" + +# ─── Cilium ─────────────────────────────────────────────────────────────────── +cilium_version: "1.15.5" +cilium_namespace: "kube-system" +cilium_chart_repo: "https://helm.cilium.io" + +# IP k3s API server (обычно kube_vip_address если используется kube-vip) +cilium_k8s_service_host: "{{ kube_vip_address | default(hostvars[groups['k3s_master'][0]]['ansible_host']) }}" +cilium_k8s_service_port: 6443 + +# Включить Hubble (observability) +cilium_hubble_enabled: true +cilium_hubble_ui_enabled: false + +# Ресурсы оператора +cilium_operator_replicas: 1 diff --git a/roles/cni/tasks/calico.yml b/roles/cni/tasks/calico.yml new file mode 100644 index 0000000..5e8453d --- /dev/null +++ b/roles/cni/tasks/calico.yml @@ -0,0 +1,36 @@ +--- +- name: Apply Tigera operator + ansible.builtin.command: > + k3s kubectl apply -f {{ calico_operator_url }} + register: calico_operator + changed_when: "'created' in calico_operator.stdout or 'configured' in calico_operator.stdout" + retries: 3 + delay: 10 + +- name: Wait for Tigera operator to be ready + ansible.builtin.command: > + k3s kubectl -n tigera-operator wait deployment/tigera-operator + --for=condition=Available --timeout=120s + changed_when: false + retries: 6 + delay: 10 + +- name: Template Calico Installation CR + ansible.builtin.template: + src: calico-installation.yaml.j2 + dest: /tmp/calico-installation.yaml + mode: '0644' + +- name: Apply Calico Installation CR + ansible.builtin.command: k3s kubectl apply -f /tmp/calico-installation.yaml + register: calico_install + changed_when: "'created' in calico_install.stdout or 'configured' in calico_install.stdout" + +- name: Wait for Calico nodes to be ready + ansible.builtin.command: > + k3s kubectl -n {{ calico_namespace }} wait pod + -l k8s-app=calico-node + --for=condition=Ready --timeout=180s + changed_when: false + retries: 6 + delay: 15 diff --git a/roles/cni/tasks/cilium.yml b/roles/cni/tasks/cilium.yml new file mode 100644 index 0000000..3cd7f90 --- /dev/null +++ b/roles/cni/tasks/cilium.yml @@ -0,0 +1,36 @@ +--- +- name: Add Cilium Helm repo + kubernetes.core.helm_repository: + name: cilium + repo_url: "{{ cilium_chart_repo }}" + +- name: Install Cilium via Helm + kubernetes.core.helm: + name: cilium + chart_ref: cilium/cilium + chart_version: "{{ cilium_version }}" + release_namespace: "{{ cilium_namespace }}" + create_namespace: false + kubeconfig: /etc/rancher/k3s/k3s.yaml + values: + k8sServiceHost: "{{ cilium_k8s_service_host }}" + k8sServicePort: "{{ cilium_k8s_service_port }}" + ipam: + mode: kubernetes + operator: + replicas: "{{ cilium_operator_replicas }}" + hubble: + enabled: "{{ cilium_hubble_enabled }}" + ui: + enabled: "{{ cilium_hubble_ui_enabled }}" + kubeProxyReplacement: true + register: cilium_deploy + +- name: Wait for Cilium pods to be ready + ansible.builtin.command: > + k3s kubectl -n {{ cilium_namespace }} wait pod + -l k8s-app=cilium + --for=condition=Ready --timeout=180s + changed_when: false + retries: 6 + delay: 15 diff --git a/roles/cni/tasks/main.yml b/roles/cni/tasks/main.yml new file mode 100644 index 0000000..ab23e90 --- /dev/null +++ b/roles/cni/tasks/main.yml @@ -0,0 +1,12 @@ +--- +- name: Skip CNI role when using built-in Flannel + ansible.builtin.meta: end_play + when: k3s_cni | default('flannel') == 'flannel' + +- name: Install Calico CNI + ansible.builtin.include_tasks: calico.yml + when: k3s_cni == 'calico' + +- name: Install Cilium CNI + ansible.builtin.include_tasks: cilium.yml + when: k3s_cni == 'cilium' diff --git a/roles/cni/templates/calico-installation.yaml.j2 b/roles/cni/templates/calico-installation.yaml.j2 new file mode 100644 index 0000000..893f2af --- /dev/null +++ b/roles/cni/templates/calico-installation.yaml.j2 @@ -0,0 +1,13 @@ +--- +apiVersion: operator.tigera.io/v1 +kind: Installation +metadata: + name: default +spec: + calicoNetwork: + ipPools: + - blockSize: 26 + cidr: "{{ calico_pod_cidr }}" + encapsulation: "{{ calico_encapsulation }}" + natOutgoing: Enabled + nodeSelector: all() diff --git a/roles/csi-nfs/defaults/main.yml b/roles/csi-nfs/defaults/main.yml new file mode 100644 index 0000000..5af3b03 --- /dev/null +++ b/roles/csi-nfs/defaults/main.yml @@ -0,0 +1,29 @@ +--- +# Версия CSI NFS Driver +csi_nfs_version: "v4.8.0" + +# Helm chart +csi_nfs_chart_repo: "https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts" +csi_nfs_chart_name: "csi-driver-nfs" +csi_nfs_namespace: "kube-system" + +# NFS сервер — IP или hostname машины с NFS +# Обычно это master нода или отдельный NFS сервер +csi_nfs_server: "{{ hostvars[groups['nfs_server'][0]]['ansible_host'] | default(hostvars[groups['k3s_master'][0]]['ansible_host']) }}" + +# Базовый путь экспорта на NFS сервере +csi_nfs_share: "/srv/nfs/k8s" + +# StorageClass настройки +# Имя включает hostname NFS сервера: nfs-master01, nfs-storage01, etc. +csi_nfs_storageclass_name: "nfs-{{ groups['nfs_server'][0] | default(groups['k3s_master'][0]) }}" +csi_nfs_storageclass_default: true +csi_nfs_reclaim_policy: "Delete" # Delete | Retain +csi_nfs_volume_binding_mode: "Immediate" + +# Монтирование подпапок для каждого PVC +# onDelete: delete | retain | archive +csi_nfs_on_delete: "delete" + +# nfs-common нужен на всех нодах для монтирования +csi_nfs_install_client: true diff --git a/roles/csi-nfs/meta/main.yml b/roles/csi-nfs/meta/main.yml new file mode 100644 index 0000000..31b30cb --- /dev/null +++ b/roles/csi-nfs/meta/main.yml @@ -0,0 +1,8 @@ +--- +galaxy_info: + author: "your-name" + description: "Deploy CSI NFS Driver and StorageClass for K3S" + license: "MIT" + min_ansible_version: "2.12" +dependencies: + - role: nfs-server diff --git a/roles/csi-nfs/tasks/install_helm.yml b/roles/csi-nfs/tasks/install_helm.yml new file mode 100644 index 0000000..328dd8b --- /dev/null +++ b/roles/csi-nfs/tasks/install_helm.yml @@ -0,0 +1,27 @@ +--- +- name: Check if Helm is installed + ansible.builtin.command: helm version --short + register: helm_check + failed_when: false + changed_when: false + become: true + +- name: Download and install Helm + ansible.builtin.shell: | + set -o pipefail + curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + args: + executable: /bin/bash + become: true + when: helm_check.rc != 0 + retries: 3 + delay: 5 + +- name: Install kubernetes.core collection (for Helm module) + ansible.builtin.command: > + ansible-galaxy collection install kubernetes.core --upgrade + delegate_to: localhost + become: false + changed_when: true + run_once: true + failed_when: false diff --git a/roles/csi-nfs/tasks/main.yml b/roles/csi-nfs/tasks/main.yml new file mode 100644 index 0000000..722557e --- /dev/null +++ b/roles/csi-nfs/tasks/main.yml @@ -0,0 +1,102 @@ +--- +- name: Install NFS client on all K3S nodes + ansible.builtin.apt: + name: nfs-common + state: present + update_cache: true + become: true + when: csi_nfs_install_client + # Выполняется на ВСЕХ нодах кластера (master + workers) + +- name: Install Helm (if not present) + ansible.builtin.include_tasks: install_helm.yml + run_once: true + delegate_to: "{{ groups['k3s_master'][0] }}" + +- name: Add CSI NFS Helm repo + kubernetes.core.helm_repository: + name: "{{ csi_nfs_chart_name }}" + repo_url: "{{ csi_nfs_chart_repo }}" + run_once: true + delegate_to: "{{ groups['k3s_master'][0] }}" + environment: + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + become: true + +- name: Deploy CSI NFS Driver via Helm + kubernetes.core.helm: + name: "{{ csi_nfs_chart_name }}" + chart_ref: "{{ csi_nfs_chart_name }}/{{ csi_nfs_chart_name }}" + chart_version: "{{ csi_nfs_version }}" + release_namespace: "{{ csi_nfs_namespace }}" + create_namespace: false + wait: true + wait_condition: + type: Ready + timeout: "5m0s" + values: + controller: + replicas: 1 + resources: + limits: + cpu: 200m + memory: 200Mi + requests: + cpu: 10m + memory: 20Mi + node: + resources: + limits: + cpu: 100m + memory: 100Mi + requests: + cpu: 10m + memory: 20Mi + run_once: true + delegate_to: "{{ groups['k3s_master'][0] }}" + environment: + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + become: true + +- name: Deploy NFS StorageClass + ansible.builtin.template: + src: storageclass.yaml.j2 + dest: /tmp/nfs-storageclass.yaml + mode: '0644' + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + +- name: Apply NFS StorageClass + ansible.builtin.command: > + k3s kubectl apply -f /tmp/nfs-storageclass.yaml + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + changed_when: true + +- name: Verify CSI NFS pods are running + ansible.builtin.command: > + k3s kubectl -n {{ csi_nfs_namespace }} get pods + -l app=csi-nfs-controller + -o jsonpath='{.items[*].status.phase}' + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + register: csi_pods + until: "'Running' in csi_pods.stdout" + retries: 20 + delay: 10 + changed_when: false + +- name: Show StorageClass + ansible.builtin.command: k3s kubectl get storageclass + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + register: sc_list + changed_when: false + +- name: Display StorageClasses + ansible.builtin.debug: + msg: "{{ sc_list.stdout_lines }}" + run_once: true diff --git a/roles/csi-nfs/templates/storageclass.yaml.j2 b/roles/csi-nfs/templates/storageclass.yaml.j2 new file mode 100644 index 0000000..bf2d4ef --- /dev/null +++ b/roles/csi-nfs/templates/storageclass.yaml.j2 @@ -0,0 +1,23 @@ +--- +# NFS StorageClass — управляется Ansible (roles/csi-nfs) +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: {{ csi_nfs_storageclass_name }} + annotations: + storageclass.kubernetes.io/is-default-class: "{{ 'true' if csi_nfs_storageclass_default else 'false' }}" +provisioner: nfs.csi.k8s.io +parameters: + server: {{ csi_nfs_server }} + share: {{ csi_nfs_share }} + # Создавать подпапку для каждого PVC (рекомендуется) + subDir: ${pvc.metadata.namespace}/${pvc.metadata.name}/${pv.metadata.name} + onDelete: {{ csi_nfs_on_delete }} +reclaimPolicy: {{ csi_nfs_reclaim_policy }} +volumeBindingMode: {{ csi_nfs_volume_binding_mode }} +mountOptions: + - nfsvers=4.1 + - hard + - intr + - timeo=600 + - retrans=3 diff --git a/roles/ingress-nginx/defaults/main.yml b/roles/ingress-nginx/defaults/main.yml new file mode 100644 index 0000000..e4441eb --- /dev/null +++ b/roles/ingress-nginx/defaults/main.yml @@ -0,0 +1,48 @@ +--- +# Версия ingress-nginx +ingress_nginx_version: "4.10.1" # Helm chart version +ingress_nginx_namespace: "ingress-nginx" + +# Helm repo +ingress_nginx_chart_repo: "https://kubernetes.github.io/ingress-nginx" +ingress_nginx_chart_name: "ingress-nginx" + +# Тип сервиса: LoadBalancer (с kube-vip) или NodePort +ingress_nginx_service_type: "LoadBalancer" + +# Если LoadBalancer — статический IP (из пула kube-vip) +# Оставь пустым для автоматического назначения +ingress_nginx_load_balancer_ip: "" + +# NodePort порты (используются когда service_type = NodePort) +ingress_nginx_http_nodeport: 30080 +ingress_nginx_https_nodeport: 30443 + +# Количество реплик контроллера +ingress_nginx_replica_count: 1 + +# Включить Prometheus метрики +ingress_nginx_metrics_enabled: false + +# Использовать DaemonSet вместо Deployment (рекомендуется для edge/RPi кластеров) +ingress_nginx_use_daemonset: false + +# Дополнительные аргументы контроллера +ingress_nginx_extra_args: {} +# Пример: +# ingress_nginx_extra_args: +# enable-ssl-passthrough: "" +# default-ssl-certificate: "default/my-tls-secret" + +# IngressClass +ingress_nginx_class_name: "nginx" +ingress_nginx_set_default_class: true + +# Ресурсы контроллера +ingress_nginx_resources: + requests: + cpu: 100m + memory: 90Mi + limits: + cpu: 500m + memory: 256Mi diff --git a/roles/ingress-nginx/handlers/main.yml b/roles/ingress-nginx/handlers/main.yml new file mode 100644 index 0000000..058af94 --- /dev/null +++ b/roles/ingress-nginx/handlers/main.yml @@ -0,0 +1,8 @@ +--- +- name: Restart K3S server + ansible.builtin.systemd: + name: k3s + state: restarted + daemon_reload: true + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" diff --git a/roles/ingress-nginx/meta/main.yml b/roles/ingress-nginx/meta/main.yml new file mode 100644 index 0000000..da1501c --- /dev/null +++ b/roles/ingress-nginx/meta/main.yml @@ -0,0 +1,8 @@ +--- +galaxy_info: + author: "your-name" + description: "Deploy ingress-nginx controller via Helm for K3S" + license: "MIT" + min_ansible_version: "2.12" +dependencies: + - role: kube-vip diff --git a/roles/ingress-nginx/tasks/main.yml b/roles/ingress-nginx/tasks/main.yml new file mode 100644 index 0000000..51d55d6 --- /dev/null +++ b/roles/ingress-nginx/tasks/main.yml @@ -0,0 +1,103 @@ +--- +- name: Disable K3S built-in Traefik (required before ingress-nginx) + ansible.builtin.lineinfile: + path: "{{ k3s_config_dir }}/config.yaml" + line: "disable: traefik" + regexp: "^disable:" + state: present + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + notify: Restart K3S server + when: not k3s_disable_traefik + +- name: Flush handlers (restart K3S if Traefik was just disabled) + ansible.builtin.meta: flush_handlers + +- name: Ensure ingress-nginx namespace exists + ansible.builtin.command: > + k3s kubectl create namespace {{ ingress_nginx_namespace }} + --dry-run=client -o yaml | k3s kubectl apply -f - + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + changed_when: false + +- name: Add ingress-nginx Helm repo + kubernetes.core.helm_repository: + name: "{{ ingress_nginx_chart_name }}" + repo_url: "{{ ingress_nginx_chart_repo }}" + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + environment: + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + +- name: Template Helm values + ansible.builtin.template: + src: ingress-nginx-values.yaml.j2 + dest: /tmp/ingress-nginx-values.yaml + mode: '0644' + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + +- name: Deploy ingress-nginx via Helm + kubernetes.core.helm: + name: "{{ ingress_nginx_chart_name }}" + chart_ref: "{{ ingress_nginx_chart_name }}/{{ ingress_nginx_chart_name }}" + chart_version: "{{ ingress_nginx_version }}" + release_namespace: "{{ ingress_nginx_namespace }}" + create_namespace: true + wait: true + timeout: "5m0s" + values_files: + - /tmp/ingress-nginx-values.yaml + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + environment: + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + +- name: Wait for ingress-nginx controller to be ready + ansible.builtin.command: > + k3s kubectl -n {{ ingress_nginx_namespace }} rollout status + deployment/{{ ingress_nginx_chart_name }}-controller + --timeout=180s + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + register: nginx_rollout + changed_when: false + retries: 3 + delay: 10 + until: nginx_rollout.rc == 0 + +- name: Get ingress-nginx service info + ansible.builtin.command: > + k3s kubectl -n {{ ingress_nginx_namespace }} + get svc {{ ingress_nginx_chart_name }}-controller + -o wide + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + register: nginx_svc + changed_when: false + +- name: Show ingress-nginx service + ansible.builtin.debug: + msg: "{{ nginx_svc.stdout_lines }}" + run_once: true + +- name: Deploy test IngressClass (verify) + ansible.builtin.command: > + k3s kubectl get ingressclass {{ ingress_nginx_class_name }} + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + register: ingress_class_check + changed_when: false + failed_when: false + +- name: Show IngressClass status + ansible.builtin.debug: + msg: "IngressClass '{{ ingress_nginx_class_name }}': {{ 'OK' if ingress_class_check.rc == 0 else 'NOT FOUND' }}" + run_once: true diff --git a/roles/ingress-nginx/templates/ingress-nginx-values.yaml.j2 b/roles/ingress-nginx/templates/ingress-nginx-values.yaml.j2 new file mode 100644 index 0000000..fb0c883 --- /dev/null +++ b/roles/ingress-nginx/templates/ingress-nginx-values.yaml.j2 @@ -0,0 +1,96 @@ +## ingress-nginx Helm values +## Управляется Ansible (roles/ingress-nginx) + +controller: + ingressClassResource: + name: "{{ ingress_nginx_class_name }}" + enabled: true + default: {{ ingress_nginx_set_default_class | lower }} + + ingressClass: "{{ ingress_nginx_class_name }}" + +{% if ingress_nginx_use_daemonset %} + kind: DaemonSet +{% else %} + kind: Deployment + replicaCount: {{ ingress_nginx_replica_count }} +{% endif %} + + service: + type: {{ ingress_nginx_service_type }} +{% if ingress_nginx_service_type == "LoadBalancer" and ingress_nginx_load_balancer_ip %} + loadBalancerIP: "{{ ingress_nginx_load_balancer_ip }}" +{% endif %} +{% if ingress_nginx_service_type == "NodePort" %} + nodePorts: + http: {{ ingress_nginx_http_nodeport }} + https: {{ ingress_nginx_https_nodeport }} +{% endif %} + + resources: + requests: + cpu: "{{ ingress_nginx_resources.requests.cpu }}" + memory: "{{ ingress_nginx_resources.requests.memory }}" + limits: + cpu: "{{ ingress_nginx_resources.limits.cpu }}" + memory: "{{ ingress_nginx_resources.limits.memory }}" + + # Логирование в JSON для удобного парсинга + config: + log-format-upstream: >- + {"time":"$time_iso8601","remote_addr":"$remote_addr", + "x_forwarded_for":"$http_x_forwarded_for","request_id":"$req_id", + "remote_user":"$remote_user","bytes_sent":"$bytes_sent", + "request_time":"$request_time","status":"$status", + "vhost":"$host","request_proto":"$server_protocol", + "path":"$uri","request_query":"$args", + "request_length":"$request_length","duration":"$request_time", + "method":"$request_method","http_referrer":"$http_referer", + "http_user_agent":"$http_user_agent"} + use-forwarded-headers: "true" + compute-full-forwarded-for: "true" + use-proxy-protocol: "false" + proxy-body-size: "50m" + proxy-read-timeout: "600" + proxy-send-timeout: "600" + +{% if ingress_nginx_extra_args %} + extraArgs: +{% for key, value in ingress_nginx_extra_args.items() %} + {{ key }}: "{{ value }}" +{% endfor %} +{% endif %} + + metrics: + enabled: {{ ingress_nginx_metrics_enabled | lower }} +{% if ingress_nginx_metrics_enabled %} + serviceMonitor: + enabled: false # включи если есть Prometheus Operator +{% endif %} + + # Tolerations для запуска на мастере и RPi + tolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + - key: "node-type" + operator: "Equal" + value: "raspberry-pi" + effect: "NoSchedule" + + admissionWebhooks: + enabled: true + failurePolicy: Fail + +defaultBackend: + enabled: true + image: + registry: registry.k8s.io + image: defaultbackend-amd64 + resources: + limits: + cpu: 10m + memory: 20Mi + requests: + cpu: 10m + memory: 20Mi diff --git a/roles/istio/defaults/main.yml b/roles/istio/defaults/main.yml new file mode 100644 index 0000000..acc9d07 --- /dev/null +++ b/roles/istio/defaults/main.yml @@ -0,0 +1,63 @@ +--- +# Включить установку Istio (false = пропустить) +istio_enabled: false + +istio_version: "1.22.2" # Helm chart version (совпадает с версией Istio) +istio_namespace: "istio-system" + +istio_chart_repo: "https://istio-release.storage.googleapis.com/charts" + +# Устанавливать Istio Ingress Gateway (LoadBalancer) +istio_install_gateway: true + +# Включить mutual TLS между сервисами +istio_mtls_mode: "STRICT" # STRICT | PERMISSIVE | DISABLE + +# Ресурсы istiod (control plane) +istio_pilot_resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + +# Ресурсы gateway +istio_gateway_resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + +# Включить Prometheus-совместимый сбор метрик +istio_telemetry_enabled: true + +# ─── Kiali (Service Mesh UI) ────────────────────────────────────────────────── +# Установка по желанию (истио должен быть включён) +kiali_enabled: false + +kiali_version: "1.86.0" # Helm chart version +kiali_namespace: "{{ istio_namespace }}" +kiali_chart_repo: "https://kiali.org/helm-charts" + +# Токен для входа в Kiali UI. +# Задай в group_vars/all/vault.yml: vault_kiali_token: "ваш-токен" +# После первой установки Ansible выведет сгенерированный токен — +# скопируй его в vault.yml для последующих запусков. +kiali_token: "{{ vault_kiali_token | default('') }}" + +# Ingress для Kiali (требует ingress-nginx) +kiali_ingress_enabled: false +kiali_ingress_host: "kiali.local" +kiali_ingress_class: "nginx" + +# Ресурсы Kiali +kiali_resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 256Mi diff --git a/roles/istio/meta/main.yml b/roles/istio/meta/main.yml new file mode 100644 index 0000000..9e2ff0f --- /dev/null +++ b/roles/istio/meta/main.yml @@ -0,0 +1,6 @@ +--- +galaxy_info: + role_name: istio + description: Deploy Istio service mesh via Helm on K3S + min_ansible_version: "2.14" +dependencies: [] diff --git a/roles/istio/molecule/default/converge.yml b/roles/istio/molecule/default/converge.yml new file mode 100644 index 0000000..3b0e070 --- /dev/null +++ b/roles/istio/molecule/default/converge.yml @@ -0,0 +1,76 @@ +--- +- name: Converge — istio role template tests + hosts: all + become: false + gather_facts: false + + vars: + istio_enabled: true + istio_version: "1.22.2" + istio_namespace: "istio-system" + istio_mtls_mode: "STRICT" + istio_install_gateway: true + istio_telemetry_enabled: true + + istio_pilot_resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + + istio_gateway_resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + + kiali_enabled: true + kiali_namespace: "istio-system" + kiali_auth_strategy: "token" + kiali_ingress_enabled: false + kiali_ingress_host: "kiali.local" + kiali_ingress_class: "nginx" + kiali_resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 256Mi + + # Vars для kiali-values.yaml.j2 (интеграция с prometheus-stack) + prometheus_stack_enabled: true + prometheus_stack_release_name: "prom" + prometheus_stack_namespace: "monitoring" + prometheus_grafana_enabled: true + grafana_admin_user: "admin" + prometheus_grafana_admin_password: "molecule-test-pass" + + tasks: + - name: Render istiod Helm values + ansible.builtin.template: + src: "{{ playbook_dir }}/../../templates/istiod-values.yaml.j2" + dest: /tmp/istiod-values.yaml + mode: '0644' + + - name: Render Kiali Helm values + ansible.builtin.template: + src: "{{ playbook_dir }}/../../templates/kiali-values.yaml.j2" + dest: /tmp/kiali-values.yaml + mode: '0644' + + - name: Render PeerAuthentication manifest + ansible.builtin.template: + src: "{{ playbook_dir }}/../../templates/peer-authentication.yaml.j2" + dest: /tmp/peer-authentication.yaml + mode: '0644' + + - name: Render Kiali token secret manifest + ansible.builtin.template: + src: "{{ playbook_dir }}/../../templates/kiali-token-secret.yaml.j2" + dest: /tmp/kiali-token-secret.yaml + mode: '0644' diff --git a/roles/istio/molecule/default/molecule.yml b/roles/istio/molecule/default/molecule.yml new file mode 100644 index 0000000..a87fcd0 --- /dev/null +++ b/roles/istio/molecule/default/molecule.yml @@ -0,0 +1,25 @@ +--- +driver: + name: docker + +platforms: + - name: istio-test + image: geerlingguy/docker-ubuntu2204-ansible:latest + pre_build_image: true + +provisioner: + name: ansible + playbooks: + converge: converge.yml + verify: verify.yml + config_options: + defaults: + interpreter_python: auto_silent + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint diff --git a/roles/istio/molecule/default/verify.yml b/roles/istio/molecule/default/verify.yml new file mode 100644 index 0000000..b3b755b --- /dev/null +++ b/roles/istio/molecule/default/verify.yml @@ -0,0 +1,107 @@ +--- +- name: Verify — istio role templates + hosts: all + become: false + gather_facts: false + + tasks: + # ── istiod Helm values ─────────────────────────────────────────────────────── + - name: Read istiod values + ansible.builtin.slurp: + src: /tmp/istiod-values.yaml + register: istiod_raw + + - name: Parse istiod YAML + ansible.builtin.set_fact: + istiod: "{{ istiod_raw.content | b64decode | from_yaml }}" + + - name: Assert istiod pilot resources exist + ansible.builtin.assert: + that: + - istiod.pilot is defined + - istiod.pilot.resources.requests.cpu == '100m' + - istiod.pilot.resources.limits.memory == '512Mi' + fail_msg: "istiod pilot resources настроены неверно" + + - name: Assert meshConfig exists + ansible.builtin.assert: + that: istiod.meshConfig is defined + fail_msg: "meshConfig отсутствует в istiod values" + + - name: Assert telemetry flag + ansible.builtin.assert: + that: istiod.meshConfig.enablePrometheusMerge == true + fail_msg: "enablePrometheusMerge должен быть true при istio_telemetry_enabled=true" + + # ── Kiali Helm values ──────────────────────────────────────────────────────── + - name: Read kiali values + ansible.builtin.slurp: + src: /tmp/kiali-values.yaml + register: kiali_raw + + - name: Parse kiali YAML + ansible.builtin.set_fact: + kiali: "{{ kiali_raw.content | b64decode | from_yaml }}" + + - name: Assert kiali auth strategy is token + ansible.builtin.assert: + that: kiali.auth.strategy == 'token' + fail_msg: "Kiali auth.strategy должен быть 'token', получено: {{ kiali.auth.strategy }}" + + - name: Assert kiali external_services prometheus URL + ansible.builtin.assert: + that: + - kiali.external_services.prometheus.url is defined + - "'prom-kube-prometheus-stack-prometheus' in kiali.external_services.prometheus.url" + fail_msg: "Kiali Prometheus URL настроен неверно: {{ kiali.external_services.prometheus.url }}" + + - name: Assert kiali grafana integration + ansible.builtin.assert: + that: + - kiali.external_services.grafana.enabled == true + - kiali.external_services.grafana.auth.username == 'admin' + fail_msg: "Kiali Grafana интеграция настроена неверно" + + # ── PeerAuthentication ─────────────────────────────────────────────────────── + - name: Read PeerAuthentication manifest + ansible.builtin.slurp: + src: /tmp/peer-authentication.yaml + register: peer_raw + + - name: Parse PeerAuthentication YAML + ansible.builtin.set_fact: + peer_auth: "{{ peer_raw.content | b64decode | from_yaml }}" + + - name: Assert PeerAuthentication kind + ansible.builtin.assert: + that: peer_auth.kind == 'PeerAuthentication' + fail_msg: "Неверный kind: {{ peer_auth.kind }}" + + - name: Assert mTLS mode is STRICT + ansible.builtin.assert: + that: peer_auth.spec.mtls.mode == 'STRICT' + fail_msg: "mTLS mode должен быть STRICT, получено: {{ peer_auth.spec.mtls.mode }}" + + # ── Kiali Token Secret ─────────────────────────────────────────────────────── + - name: Read kiali token secret manifest + ansible.builtin.slurp: + src: /tmp/kiali-token-secret.yaml + register: kiali_secret_raw + + - name: Parse kiali token secret YAML + ansible.builtin.set_fact: + kiali_secret: "{{ kiali_secret_raw.content | b64decode | from_yaml }}" + + - name: Assert kiali secret type + ansible.builtin.assert: + that: kiali_secret.type == 'kubernetes.io/service-account-token' + fail_msg: "Неверный тип секрета: {{ kiali_secret.type }}" + + - name: Assert kiali secret annotation + ansible.builtin.assert: + that: kiali_secret.metadata.annotations['kubernetes.io/service-account.name'] == 'kiali-admin' + fail_msg: "Неверная аннотация service-account" + + - name: Summary + ansible.builtin.debug: + msg: "Все проверки istio/kiali прошли успешно" diff --git a/roles/istio/tasks/main.yml b/roles/istio/tasks/main.yml new file mode 100644 index 0000000..768b6c4 --- /dev/null +++ b/roles/istio/tasks/main.yml @@ -0,0 +1,274 @@ +--- +- name: Istio — skip if not enabled + ansible.builtin.debug: + msg: "Istio отключён (istio_enabled: false). Пропускаем." + when: not istio_enabled + run_once: true + +- name: Istio — install + when: istio_enabled + block: + - name: Install Helm (if needed) + ansible.builtin.include_tasks: "{{ playbook_dir }}/../roles/csi-nfs/tasks/install_helm.yml" + + - name: Add Istio Helm repo + kubernetes.core.helm_repository: + name: istio + repo_url: "{{ istio_chart_repo }}" + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + environment: + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + + - name: Create istio-system namespace + ansible.builtin.command: > + k3s kubectl create namespace {{ istio_namespace }} + --dry-run=client -o yaml | k3s kubectl apply -f - + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + changed_when: false + + - name: Install Istio base CRDs (istio/base) + kubernetes.core.helm: + name: istio-base + chart_ref: istio/base + chart_version: "{{ istio_version }}" + release_namespace: "{{ istio_namespace }}" + create_namespace: false + wait: true + timeout: "5m0s" + values: + defaultRevision: default + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + environment: + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + + - name: Template istiod values + ansible.builtin.template: + src: istiod-values.yaml.j2 + dest: /tmp/istiod-values.yaml + mode: '0644' + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + + - name: Install istiod (control plane) + kubernetes.core.helm: + name: istiod + chart_ref: istio/istiod + chart_version: "{{ istio_version }}" + release_namespace: "{{ istio_namespace }}" + create_namespace: false + wait: true + timeout: "5m0s" + values_files: + - /tmp/istiod-values.yaml + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + environment: + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + + - name: Wait for istiod to be ready + ansible.builtin.command: > + k3s kubectl -n {{ istio_namespace }} + rollout status deployment/istiod --timeout=180s + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + register: istiod_ready + changed_when: false + retries: 3 + delay: 10 + until: istiod_ready.rc == 0 + + - name: Install Istio Gateway + kubernetes.core.helm: + name: istio-ingressgateway + chart_ref: istio/gateway + chart_version: "{{ istio_version }}" + release_namespace: "{{ istio_namespace }}" + create_namespace: false + wait: true + timeout: "5m0s" + values: + resources: + requests: + cpu: "{{ istio_gateway_resources.requests.cpu }}" + memory: "{{ istio_gateway_resources.requests.memory }}" + limits: + cpu: "{{ istio_gateway_resources.limits.cpu }}" + memory: "{{ istio_gateway_resources.limits.memory }}" + service: + type: LoadBalancer + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + when: istio_install_gateway + environment: + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + + - name: Apply default PeerAuthentication (mTLS mode) + ansible.builtin.template: + src: peer-authentication.yaml.j2 + dest: /tmp/istio-peer-auth.yaml + mode: '0644' + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + + - name: Apply PeerAuthentication to cluster + ansible.builtin.command: > + k3s kubectl apply -f /tmp/istio-peer-auth.yaml + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + changed_when: true + + - name: Show Istio status + ansible.builtin.command: > + k3s kubectl -n {{ istio_namespace }} get pods + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + register: istio_pods + changed_when: false + + - name: Istio pods + ansible.builtin.debug: + msg: "{{ istio_pods.stdout_lines }}" + run_once: true + +# ─── Kiali ──────────────────────────────────────────────────────────────────── +- name: Kiali — skip if not enabled + ansible.builtin.debug: + msg: "Kiali отключён (kiali_enabled: false). Пропускаем." + when: istio_enabled and not kiali_enabled + run_once: true + +- name: Kiali — install + when: istio_enabled and kiali_enabled + block: + - name: Add Kiali Helm repo + kubernetes.core.helm_repository: + name: kiali + repo_url: "{{ kiali_chart_repo }}" + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + environment: + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + + - name: Create kiali-admin ServiceAccount + ansible.builtin.command: > + k3s kubectl create serviceaccount kiali-admin + -n {{ kiali_namespace }} + --dry-run=client -o yaml | k3s kubectl apply -f - + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + changed_when: false + + - name: Bind kiali-admin to cluster-admin + ansible.builtin.command: > + k3s kubectl create clusterrolebinding kiali-admin + --clusterrole=cluster-admin + --serviceaccount={{ kiali_namespace }}:kiali-admin + --dry-run=client -o yaml | k3s kubectl apply -f - + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + changed_when: false + + - name: Create long-lived token secret for kiali-admin + ansible.builtin.template: + src: kiali-token-secret.yaml.j2 + dest: /tmp/kiali-token-secret.yaml + mode: '0644' + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + + - name: Apply kiali-admin token secret + ansible.builtin.command: > + k3s kubectl apply -f /tmp/kiali-token-secret.yaml + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + changed_when: false + + - name: Wait for k8s to populate the token + ansible.builtin.command: > + k3s kubectl -n {{ kiali_namespace }} + get secret kiali-admin-token + -o jsonpath="{.data.token}" + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + register: kiali_token_check + until: kiali_token_check.stdout | length > 0 + retries: 10 + delay: 3 + changed_when: false + + - name: Decode Kiali login token + ansible.builtin.set_fact: + kiali_generated_token: "{{ kiali_token_check.stdout | b64decode }}" + run_once: true + + - name: Template Kiali Helm values + ansible.builtin.template: + src: kiali-values.yaml.j2 + dest: /tmp/kiali-values.yaml + mode: '0644' + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + + - name: Deploy Kiali via Helm + kubernetes.core.helm: + name: kiali-server + chart_ref: kiali/kiali-server + chart_version: "{{ kiali_version }}" + release_namespace: "{{ kiali_namespace }}" + create_namespace: false + wait: true + timeout: "5m0s" + values_files: + - /tmp/kiali-values.yaml + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + environment: + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + + - name: Wait for Kiali to be ready + ansible.builtin.command: > + k3s kubectl -n {{ kiali_namespace }} + rollout status deployment/kiali --timeout=180s + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + register: kiali_ready + changed_when: false + retries: 3 + delay: 10 + until: kiali_ready.rc == 0 + + - name: Show Kiali access info + ansible.builtin.debug: + msg: + - "══════════════════════════════════════════════════" + - " Kiali UI доступен через port-forward:" + - " kubectl -n {{ kiali_namespace }} port-forward svc/kiali 20001:20001" + - " Откройте: http://localhost:20001" + - "{% if kiali_ingress_enabled %} Или через Ingress: http://{{ kiali_ingress_host }}{% endif %}" + - "" + - " Стратегия аутентификации: token" + - " Токен для входа:" + - " {{ kiali_generated_token }}" + - "" + - " Сохрани токен в vault.yml:" + - " vault_kiali_token: <токен выше>" + - "══════════════════════════════════════════════════" + run_once: true diff --git a/roles/istio/templates/istiod-values.yaml.j2 b/roles/istio/templates/istiod-values.yaml.j2 new file mode 100644 index 0000000..302cb40 --- /dev/null +++ b/roles/istio/templates/istiod-values.yaml.j2 @@ -0,0 +1,36 @@ +## istiod Helm values +## Управляется Ansible (roles/istio) + +pilot: + resources: + requests: + cpu: "{{ istio_pilot_resources.requests.cpu }}" + memory: "{{ istio_pilot_resources.requests.memory }}" + limits: + cpu: "{{ istio_pilot_resources.limits.cpu }}" + memory: "{{ istio_pilot_resources.limits.memory }}" + + # Tolerations — можно запускать на мастерах + tolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + +meshConfig: + accessLogFile: /dev/stdout + enableTracing: false +{% if istio_telemetry_enabled %} + defaultConfig: + proxyMetadata: {} + enablePrometheusMerge: true +{% endif %} + +global: + proxy: + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi diff --git a/roles/istio/templates/kiali-token-secret.yaml.j2 b/roles/istio/templates/kiali-token-secret.yaml.j2 new file mode 100644 index 0000000..c00d1e9 --- /dev/null +++ b/roles/istio/templates/kiali-token-secret.yaml.j2 @@ -0,0 +1,10 @@ +## Долгосрочный токен для ServiceAccount kiali-admin +## k8s автоматически заполняет поле .data.token +apiVersion: v1 +kind: Secret +metadata: + name: kiali-admin-token + namespace: {{ kiali_namespace }} + annotations: + kubernetes.io/service-account.name: kiali-admin +type: kubernetes.io/service-account-token diff --git a/roles/istio/templates/kiali-values.yaml.j2 b/roles/istio/templates/kiali-values.yaml.j2 new file mode 100644 index 0000000..6aaca5b --- /dev/null +++ b/roles/istio/templates/kiali-values.yaml.j2 @@ -0,0 +1,64 @@ +## Kiali Helm values +## Управляется Ansible (roles/istio) + +auth: + strategy: token + +deployment: + # Использовать существующий ServiceAccount kiali-admin + service_account: kiali-admin + resources: + requests: + cpu: "{{ kiali_resources.requests.cpu }}" + memory: "{{ kiali_resources.requests.memory }}" + limits: + cpu: "{{ kiali_resources.limits.cpu }}" + memory: "{{ kiali_resources.limits.memory }}" + tolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + +{% if kiali_ingress_enabled %} + ingress: + enabled: true + class_name: "{{ kiali_ingress_class }}" + override_yaml: + spec: + rules: + - host: "{{ kiali_ingress_host }}" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: kiali + port: + number: 20001 +{% endif %} + +external_services: + prometheus: +{% if prometheus_stack_enabled %} + url: "http://{{ prometheus_stack_release_name }}-kube-prometheus-stack-prometheus.{{ prometheus_stack_namespace }}:9090" +{% else %} + url: "http://prometheus-operated.monitoring:9090" +{% endif %} + + grafana: + enabled: {{ prometheus_grafana_enabled | lower }} +{% if prometheus_stack_enabled and prometheus_grafana_enabled %} + url: "http://{{ prometheus_stack_release_name }}-grafana.{{ prometheus_stack_namespace }}:80" + auth: + username: "{{ grafana_admin_user }}" + password: "{{ prometheus_grafana_admin_password }}" + type: basic +{% endif %} + + istio: + root_namespace: "{{ istio_namespace }}" + +server: + port: 20001 + web_root: /kiali diff --git a/roles/istio/templates/peer-authentication.yaml.j2 b/roles/istio/templates/peer-authentication.yaml.j2 new file mode 100644 index 0000000..20781d0 --- /dev/null +++ b/roles/istio/templates/peer-authentication.yaml.j2 @@ -0,0 +1,9 @@ +## Глобальный режим mTLS для всего mesh +apiVersion: security.istio.io/v1beta1 +kind: PeerAuthentication +metadata: + name: default + namespace: {{ istio_namespace }} +spec: + mtls: + mode: {{ istio_mtls_mode }} diff --git a/roles/k3s/defaults/main.yml b/roles/k3s/defaults/main.yml new file mode 100644 index 0000000..1c8cd28 --- /dev/null +++ b/roles/k3s/defaults/main.yml @@ -0,0 +1,19 @@ +--- +# Token for joining cluster nodes (override in vault!) +k3s_token: "changeme-use-ansible-vault" + +# Master node IP (used by agents to join) +k3s_master_ip: "{{ hostvars[groups['k3s_master'][0]]['ansible_host'] }}" + +# K3S API server URL +k3s_api_url: "https://{{ k3s_master_ip }}:6443" + +# Write kubeconfig to local machine +k3s_fetch_kubeconfig: true +k3s_kubeconfig_local_path: "./kubeconfig" + +# Raspberry Pi specific +rpi_cgroup_enable: true +rpi_cmdline_path: /boot/cmdline.txt +# For newer RPi OS (bookworm) +rpi_cmdline_path_new: /boot/firmware/cmdline.txt diff --git a/roles/k3s/handlers/main.yml b/roles/k3s/handlers/main.yml new file mode 100644 index 0000000..036705d --- /dev/null +++ b/roles/k3s/handlers/main.yml @@ -0,0 +1,21 @@ +--- +- name: Restart K3S server + ansible.builtin.systemd: + name: k3s + state: restarted + daemon_reload: true + become: "{{ k3s_become }}" + +- name: Restart K3S agent + ansible.builtin.systemd: + name: k3s-agent + state: restarted + daemon_reload: true + become: "{{ k3s_become }}" + +- name: Reboot Raspberry Pi + ansible.builtin.reboot: + reboot_timeout: 300 + connect_timeout: 60 + msg: "Rebooting after enabling cgroups for K3S" + become: "{{ k3s_become }}" diff --git a/roles/k3s/meta/main.yml b/roles/k3s/meta/main.yml new file mode 100644 index 0000000..dd41786 --- /dev/null +++ b/roles/k3s/meta/main.yml @@ -0,0 +1,14 @@ +--- +galaxy_info: + author: "your-name" + description: "Install and configure K3S Kubernetes cluster" + license: "MIT" + min_ansible_version: "2.12" + platforms: + - name: Ubuntu + versions: ["20.04", "22.04", "24.04"] + - name: Debian + versions: ["11", "12"] + - name: Raspbian + versions: ["11", "12"] +dependencies: [] diff --git a/roles/k3s/molecule/default/converge.yml b/roles/k3s/molecule/default/converge.yml new file mode 100644 index 0000000..e3f5675 --- /dev/null +++ b/roles/k3s/molecule/default/converge.yml @@ -0,0 +1,56 @@ +--- +- name: Converge — k3s role unit tests + hosts: all + become: true + gather_facts: true + + vars: + k3s_token: "molecule-test-token-abc123" + k3s_version: "v1.29.3+k3s1" + k3s_become: true + k3s_fetch_kubeconfig: false + k3s_node_labels: [] + k3s_node_taints: [] + k3s_cluster_cidr: "10.42.0.0/16" + k3s_service_cidr: "10.43.0.0/16" + k3s_cluster_dns: "10.43.0.10" + k3s_flannel_backend: "vxlan" + k3s_install_dir: /usr/local/bin + k3s_config_dir: /etc/rancher/k3s + k3s_data_dir: /var/lib/rancher/k3s + k3s_disable_traefik: true + k3s_disable_servicelb: false + k3s_disable_local_storage: false + k3s_extra_server_args: "" + k3s_api_url: "https://127.0.0.1:6443" + + pre_tasks: + - name: Mock k3s binary (симулирует уже установленный k3s) + ansible.builtin.copy: + content: "#!/bin/bash\nexit 0\n" + dest: /usr/local/bin/k3s + mode: '0755' + + - name: Create k3s server data directory + ansible.builtin.file: + path: /var/lib/rancher/k3s/server + state: directory + mode: '0700' + + - name: Mock k3s node-token + ansible.builtin.copy: + content: "K10::server:molecule-test-node-token\n" + dest: /var/lib/rancher/k3s/server/node-token + mode: '0600' + + tasks: + # ── Тест 1: Предварительные требования ───────────────────────────────────── + - name: Test prereqs — install packages + ansible.builtin.include_tasks: "{{ playbook_dir }}/../../tasks/prereqs.yml" + + # ── Тест 2: Генерация конфигурационного файла ─────────────────────────────── + - name: Test server config template rendering + ansible.builtin.template: + src: "{{ playbook_dir }}/../../templates/k3s-server-config.yaml.j2" + dest: /etc/rancher/k3s/config.yaml + mode: '0600' diff --git a/roles/k3s/molecule/default/molecule.yml b/roles/k3s/molecule/default/molecule.yml new file mode 100644 index 0000000..352ba8b --- /dev/null +++ b/roles/k3s/molecule/default/molecule.yml @@ -0,0 +1,38 @@ +--- +driver: + name: docker + +platforms: + - name: k3s-node + image: geerlingguy/docker-ubuntu2204-ansible:latest + pre_build_image: true + privileged: true + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + command: /lib/systemd/systemd + groups: + - k3s_master + - k3s_cluster + +provisioner: + name: ansible + playbooks: + prepare: prepare.yml + converge: converge.yml + verify: verify.yml + config_options: + defaults: + interpreter_python: auto_silent + inventory: + group_vars: + k3s_master: + k3s_master_ip: "{{ hostvars[groups['k3s_master'][0]]['ansible_host'] | default('127.0.0.1') }}" + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint diff --git a/roles/k3s/molecule/default/prepare.yml b/roles/k3s/molecule/default/prepare.yml new file mode 100644 index 0000000..ffd1316 --- /dev/null +++ b/roles/k3s/molecule/default/prepare.yml @@ -0,0 +1,28 @@ +--- +- name: Prepare k3s test environment + hosts: all + become: true + gather_facts: false + + tasks: + - name: Wait for systemd to start + ansible.builtin.command: systemctl is-system-running + register: systemd_running + until: systemd_running.stdout in ['running', 'degraded'] + retries: 20 + delay: 3 + changed_when: false + failed_when: false + + - name: Install Python3 + ansible.builtin.raw: apt-get update && apt-get install -y python3 + changed_when: true + + - name: Install Ansible collections + ansible.builtin.command: > + ansible-galaxy collection install + community.general ansible.posix --upgrade + become: false + changed_when: true + delegate_to: localhost + run_once: true diff --git a/roles/k3s/molecule/default/verify.yml b/roles/k3s/molecule/default/verify.yml new file mode 100644 index 0000000..df4d87b --- /dev/null +++ b/roles/k3s/molecule/default/verify.yml @@ -0,0 +1,104 @@ +--- +- name: Verify — k3s role + hosts: all + become: true + gather_facts: false + + tasks: + # ── Проверка директорий ───────────────────────────────────────────────────── + - name: Check k3s config directory exists + ansible.builtin.stat: + path: /etc/rancher/k3s + register: config_dir + + - name: Assert config directory + ansible.builtin.assert: + that: config_dir.stat.isdir + fail_msg: "Директория /etc/rancher/k3s не создана" + + # ── Проверка конфигурационного файла ──────────────────────────────────────── + - name: Check config file exists + ansible.builtin.stat: + path: /etc/rancher/k3s/config.yaml + register: config_file + + - name: Assert config file exists + ansible.builtin.assert: + that: config_file.stat.exists + fail_msg: "Файл /etc/rancher/k3s/config.yaml не создан" + + - name: Check config file permissions (0600) + ansible.builtin.assert: + that: "config_file.stat.mode == '0600'" + fail_msg: "Неверные права на config.yaml: {{ config_file.stat.mode }}" + + - name: Read config file + ansible.builtin.slurp: + src: /etc/rancher/k3s/config.yaml + register: config_raw + + - name: Parse config as YAML + ansible.builtin.set_fact: + k3s_config: "{{ config_raw.content | b64decode | from_yaml }}" + + - name: Assert token is set + ansible.builtin.assert: + that: k3s_config.token is defined + fail_msg: "Поле 'token' отсутствует в config.yaml" + + - name: Assert cluster-cidr is correct + ansible.builtin.assert: + that: k3s_config['cluster-cidr'] == '10.42.0.0/16' + fail_msg: "Неверный cluster-cidr: {{ k3s_config['cluster-cidr'] }}" + + - name: Assert service-cidr is correct + ansible.builtin.assert: + that: k3s_config['service-cidr'] == '10.43.0.0/16' + fail_msg: "Неверный service-cidr: {{ k3s_config['service-cidr'] }}" + + - name: Assert cluster-init is set (первый мастер) + ansible.builtin.assert: + that: k3s_config['cluster-init'] == true + fail_msg: "cluster-init должен быть true для первого мастера" + + - name: Assert traefik is disabled + ansible.builtin.assert: + that: "'traefik' in k3s_config.disable" + fail_msg: "traefik должен быть в списке disable" + + # ── Проверка системных пакетов ────────────────────────────────────────────── + - name: Check curl is installed + ansible.builtin.command: which curl + register: curl_check + changed_when: false + + - name: Assert curl installed + ansible.builtin.assert: + that: curl_check.rc == 0 + fail_msg: "curl не установлен" + + - name: Check iptables is installed + ansible.builtin.command: which iptables + register: iptables_check + changed_when: false + failed_when: false + + - name: Assert iptables installed + ansible.builtin.assert: + that: iptables_check.rc == 0 + fail_msg: "iptables не установлен" + + # ── Проверка sysctl ────────────────────────────────────────────────────────── + - name: Check ip_forward sysctl + ansible.builtin.command: sysctl net.ipv4.ip_forward + register: ip_forward + changed_when: false + + - name: Assert ip_forward is 1 + ansible.builtin.assert: + that: "'= 1' in ip_forward.stdout" + fail_msg: "net.ipv4.ip_forward не установлен в 1" + + - name: Summary + ansible.builtin.debug: + msg: "Все проверки прошли успешно для ноды {{ inventory_hostname }}" diff --git a/roles/k3s/tasks/healthcheck.yml b/roles/k3s/tasks/healthcheck.yml new file mode 100644 index 0000000..e089992 --- /dev/null +++ b/roles/k3s/tasks/healthcheck.yml @@ -0,0 +1,73 @@ +--- +# Диагностика состояния кластера + +- name: Check K3S service status (master) + ansible.builtin.systemd: + name: k3s + register: k3s_service_status + become: "{{ k3s_become }}" + when: inventory_hostname in groups['k3s_master'] + +- name: Check K3S service status (workers) + ansible.builtin.systemd: + name: k3s-agent + register: k3s_agent_status + become: "{{ k3s_become }}" + when: inventory_hostname in groups['k3s_workers'] + +- name: Show service status + ansible.builtin.debug: + msg: >- + {{ inventory_hostname }}: + {{ + (k3s_service_status.status.ActiveState | default('')) + if inventory_hostname in groups['k3s_master'] + else (k3s_agent_status.status.ActiveState | default('')) + }} + +- name: Get node list + ansible.builtin.command: k3s kubectl get nodes -o wide + register: node_list + become: "{{ k3s_become }}" + changed_when: false + run_once: true + delegate_to: "{{ groups['k3s_master'][0] }}" + +- name: Show nodes + ansible.builtin.debug: + msg: "{{ node_list.stdout_lines }}" + run_once: true + +- name: Get all pods (all namespaces) + ansible.builtin.command: k3s kubectl get pods -A --field-selector=status.phase!=Running + register: non_running_pods + become: "{{ k3s_become }}" + changed_when: false + run_once: true + delegate_to: "{{ groups['k3s_master'][0] }}" + failed_when: false + +- name: Show non-running pods (if any) + ansible.builtin.debug: + msg: "{{ non_running_pods.stdout_lines }}" + run_once: true + when: non_running_pods.stdout_lines | length > 1 + +- name: Check disk space on all nodes + ansible.builtin.shell: df -h / | tail -1 + register: disk_space + changed_when: false + become: "{{ k3s_become }}" + +- name: Show disk usage + ansible.builtin.debug: + msg: "{{ inventory_hostname }} disk: {{ disk_space.stdout }}" + +- name: Check memory usage + ansible.builtin.shell: free -h | grep Mem + register: mem_usage + changed_when: false + +- name: Show memory usage + ansible.builtin.debug: + msg: "{{ inventory_hostname }} memory: {{ mem_usage.stdout }}" diff --git a/roles/k3s/tasks/install_agent.yml b/roles/k3s/tasks/install_agent.yml new file mode 100644 index 0000000..50be72c --- /dev/null +++ b/roles/k3s/tasks/install_agent.yml @@ -0,0 +1,51 @@ +--- +- name: Check if K3S agent is already installed + ansible.builtin.stat: + path: "{{ k3s_install_dir }}/k3s" + register: k3s_agent_binary + +- name: Get node token from master + ansible.builtin.set_fact: + k3s_node_token: "{{ hostvars[groups['k3s_master'][0]]['k3s_node_token'] }}" + +- name: Write K3S agent config + ansible.builtin.template: + src: k3s-agent-config.yaml.j2 + dest: "{{ k3s_config_dir }}/config.yaml" + mode: '0600' + become: "{{ k3s_become }}" + notify: Restart K3S agent + +- name: Download and install K3S agent + ansible.builtin.shell: | + set -o pipefail + curl -sfL https://get.k3s.io | \ + INSTALL_K3S_VERSION="{{ k3s_version }}" \ + INSTALL_K3S_EXEC="agent" \ + K3S_URL="{{ k3s_api_url }}" \ + K3S_TOKEN="{{ k3s_node_token }}" \ + sh - + args: + executable: /bin/bash + become: "{{ k3s_become }}" + when: not k3s_agent_binary.stat.exists + notify: Restart K3S agent + +- name: Enable and start K3S agent service + ansible.builtin.systemd: + name: k3s-agent + enabled: true + state: started + daemon_reload: true + become: "{{ k3s_become }}" + +- name: Wait for node to join the cluster + ansible.builtin.command: > + k3s kubectl get node {{ inventory_hostname }} + delegate_to: "{{ groups['k3s_master'][0] }}" + become: "{{ k3s_become }}" + register: node_ready + until: "'Ready' in node_ready.stdout" + retries: 20 + delay: 10 + changed_when: false diff --git a/roles/k3s/tasks/install_server.yml b/roles/k3s/tasks/install_server.yml new file mode 100644 index 0000000..dc07d06 --- /dev/null +++ b/roles/k3s/tasks/install_server.yml @@ -0,0 +1,61 @@ +--- +- name: Check if K3S server is already installed + ansible.builtin.stat: + path: "{{ k3s_install_dir }}/k3s" + register: k3s_binary + +- name: Write K3S server config + ansible.builtin.template: + src: k3s-server-config.yaml.j2 + dest: "{{ k3s_config_dir }}/config.yaml" + mode: '0600' + become: "{{ k3s_become }}" + notify: Restart K3S server + +- name: Download and install K3S server + ansible.builtin.shell: | + set -o pipefail + curl -sfL https://get.k3s.io | \ + INSTALL_K3S_VERSION="{{ k3s_version }}" \ + INSTALL_K3S_EXEC="server" \ + K3S_TOKEN="{{ k3s_token }}" \ + sh - + args: + executable: /bin/bash + become: "{{ k3s_become }}" + when: not k3s_binary.stat.exists + notify: Restart K3S server + +- name: Enable and start K3S server service + ansible.builtin.systemd: + name: k3s + enabled: true + state: started + daemon_reload: true + become: "{{ k3s_become }}" + +- name: Wait for K3S node-token to be generated + ansible.builtin.wait_for: + path: /var/lib/rancher/k3s/server/node-token + timeout: 120 + become: "{{ k3s_become }}" + +- name: Read node-token + ansible.builtin.slurp: + src: /var/lib/rancher/k3s/server/node-token + register: node_token_raw + become: "{{ k3s_become }}" + +- name: Set node-token fact (shared to all hosts) + ansible.builtin.set_fact: + k3s_node_token: "{{ node_token_raw['content'] | b64decode | trim }}" + +- name: Wait for API server to become available + ansible.builtin.uri: + url: "{{ k3s_api_url }}/healthz" + validate_certs: false + status_code: 200 + register: api_health + until: api_health.status == 200 + retries: 30 + delay: 5 diff --git a/roles/k3s/tasks/kubeconfig.yml b/roles/k3s/tasks/kubeconfig.yml new file mode 100644 index 0000000..7abe882 --- /dev/null +++ b/roles/k3s/tasks/kubeconfig.yml @@ -0,0 +1,27 @@ +--- +- name: Read kubeconfig from master + ansible.builtin.slurp: + src: /etc/rancher/k3s/k3s.yaml + register: kubeconfig_raw + become: "{{ k3s_become }}" + +- name: Decode and patch server URL in kubeconfig + ansible.builtin.set_fact: + kubeconfig_patched: >- + {{ + kubeconfig_raw['content'] | b64decode + | regex_replace('https://127.0.0.1:6443', k3s_api_url) + | regex_replace('https://localhost:6443', k3s_api_url) + }} + +- name: Save kubeconfig locally + ansible.builtin.copy: + content: "{{ kubeconfig_patched }}" + dest: "{{ k3s_kubeconfig_local_path }}" + mode: '0600' + delegate_to: localhost + become: false + +- name: Show kubeconfig location + ansible.builtin.debug: + msg: "Kubeconfig saved to {{ k3s_kubeconfig_local_path }}" diff --git a/roles/k3s/tasks/main.yml b/roles/k3s/tasks/main.yml new file mode 100644 index 0000000..9122a0b --- /dev/null +++ b/roles/k3s/tasks/main.yml @@ -0,0 +1,21 @@ +--- +- name: Include OS prerequisites + ansible.builtin.include_tasks: prereqs.yml + +- name: Raspberry Pi — enable cgroups + ansible.builtin.include_tasks: rpi_cgroups.yml + when: ansible_architecture in ['armv7l', 'aarch64'] and rpi_cgroup_enable + +- name: Install K3S server + ansible.builtin.include_tasks: install_server.yml + when: inventory_hostname in groups['k3s_master'] + +- name: Configure node labels and taints + ansible.builtin.include_tasks: node_config.yml + when: k3s_node_labels | length > 0 or k3s_node_taints | length > 0 + +- name: Fetch kubeconfig to local machine + ansible.builtin.include_tasks: kubeconfig.yml + when: + - k3s_fetch_kubeconfig + - inventory_hostname == groups['k3s_master'][0] diff --git a/roles/k3s/tasks/node_config.yml b/roles/k3s/tasks/node_config.yml new file mode 100644 index 0000000..6afc8de --- /dev/null +++ b/roles/k3s/tasks/node_config.yml @@ -0,0 +1,18 @@ +--- +- name: Apply node labels + ansible.builtin.command: > + k3s kubectl label node {{ inventory_hostname }} + {{ item }} --overwrite + loop: "{{ k3s_node_labels }}" + delegate_to: "{{ groups['k3s_master'][0] }}" + become: "{{ k3s_become }}" + changed_when: true + +- name: Apply node taints + ansible.builtin.command: > + k3s kubectl taint node {{ inventory_hostname }} + {{ item }} --overwrite + loop: "{{ k3s_node_taints }}" + delegate_to: "{{ groups['k3s_master'][0] }}" + become: "{{ k3s_become }}" + changed_when: true diff --git a/roles/k3s/tasks/prereqs.yml b/roles/k3s/tasks/prereqs.yml new file mode 100644 index 0000000..5ff4a5a --- /dev/null +++ b/roles/k3s/tasks/prereqs.yml @@ -0,0 +1,67 @@ +--- +- name: Update apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + become: "{{ k3s_become }}" + +- name: Install required packages + ansible.builtin.apt: + name: + - curl + - ca-certificates + - apt-transport-https + - gnupg + - iptables + state: present + become: "{{ k3s_become }}" + +- name: Disable swap + ansible.builtin.command: swapoff -a + become: "{{ k3s_become }}" + changed_when: false + +- name: Remove swap from fstab + ansible.builtin.replace: + path: /etc/fstab + regexp: '^([^#].*?\sswap\s+sw\s+.*)$' + replace: '# \1' + become: "{{ k3s_become }}" + +- name: Load required kernel modules + community.general.modprobe: + name: "{{ item }}" + state: present + loop: + - overlay + - br_netfilter + become: "{{ k3s_become }}" + +- name: Persist kernel modules + ansible.builtin.copy: + dest: /etc/modules-load.d/k3s.conf + content: | + overlay + br_netfilter + mode: '0644' + become: "{{ k3s_become }}" + +- name: Set sysctl params for Kubernetes networking + ansible.posix.sysctl: + name: "{{ item.key }}" + value: "{{ item.value }}" + state: present + sysctl_file: /etc/sysctl.d/99-k3s.conf + reload: true + loop: + - { key: "net.bridge.bridge-nf-call-iptables", value: "1" } + - { key: "net.bridge.bridge-nf-call-ip6tables", value: "1" } + - { key: "net.ipv4.ip_forward", value: "1" } + become: "{{ k3s_become }}" + +- name: Create K3S config directory + ansible.builtin.file: + path: "{{ k3s_config_dir }}" + state: directory + mode: '0755' + become: "{{ k3s_become }}" diff --git a/roles/k3s/tasks/rpi_cgroups.yml b/roles/k3s/tasks/rpi_cgroups.yml new file mode 100644 index 0000000..0fca027 --- /dev/null +++ b/roles/k3s/tasks/rpi_cgroups.yml @@ -0,0 +1,47 @@ +--- +# Raspberry Pi requires cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory +# in /boot/cmdline.txt (legacy) or /boot/firmware/cmdline.txt (bookworm+) + +- name: Detect correct cmdline.txt path + ansible.builtin.set_fact: + rpi_cmdline_file: >- + {{ + rpi_cmdline_path_new + if ansible_distribution_release in ['bookworm', 'trixie'] + else rpi_cmdline_path + }} + +- name: Check if cmdline.txt exists + ansible.builtin.stat: + path: "{{ rpi_cmdline_file }}" + register: cmdline_stat + become: "{{ k3s_become }}" + +- name: Fallback to legacy path if new path missing + ansible.builtin.set_fact: + rpi_cmdline_file: "{{ rpi_cmdline_path }}" + when: not cmdline_stat.stat.exists + +- name: Read current cmdline.txt + ansible.builtin.slurp: + src: "{{ rpi_cmdline_file }}" + register: cmdline_content + become: "{{ k3s_become }}" + +- name: Set cgroup parameters fact + ansible.builtin.set_fact: + cmdline_current: "{{ cmdline_content['content'] | b64decode | trim }}" + cgroup_params: "cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1" + +- name: Add cgroup params to cmdline.txt if missing + ansible.builtin.copy: + dest: "{{ rpi_cmdline_file }}" + content: "{{ cmdline_current }} {{ cgroup_params }}\n" + mode: '0755' + backup: true + become: "{{ k3s_become }}" + when: "'cgroup_memory=1' not in cmdline_current" + notify: Reboot Raspberry Pi + +- name: Flush handlers to reboot if needed + ansible.builtin.meta: flush_handlers diff --git a/roles/k3s/tasks/uninstall.yml b/roles/k3s/tasks/uninstall.yml new file mode 100644 index 0000000..02d60e7 --- /dev/null +++ b/roles/k3s/tasks/uninstall.yml @@ -0,0 +1,89 @@ +--- +# Полное удаление K3S с ноды + +- name: Stop and disable K3S server service + ansible.builtin.systemd: + name: k3s + state: stopped + enabled: false + become: "{{ k3s_become }}" + failed_when: false + when: inventory_hostname in groups['k3s_master'] + +- name: Stop and disable K3S agent service + ansible.builtin.systemd: + name: k3s-agent + state: stopped + enabled: false + become: "{{ k3s_become }}" + failed_when: false + when: inventory_hostname in groups['k3s_workers'] + +- name: Run K3S server uninstall script (if exists) + ansible.builtin.command: /usr/local/bin/k3s-uninstall.sh + become: "{{ k3s_become }}" + failed_when: false + changed_when: true + when: inventory_hostname in groups['k3s_master'] + +- name: Run K3S agent uninstall script (if exists) + ansible.builtin.command: /usr/local/bin/k3s-agent-uninstall.sh + become: "{{ k3s_become }}" + failed_when: false + changed_when: true + when: inventory_hostname in groups['k3s_workers'] + +- name: Remove K3S binary + ansible.builtin.file: + path: "{{ k3s_install_dir }}/k3s" + state: absent + become: "{{ k3s_become }}" + +- name: Remove K3S config directory + ansible.builtin.file: + path: "{{ k3s_config_dir }}" + state: absent + become: "{{ k3s_become }}" + +- name: Remove K3S data directory + ansible.builtin.file: + path: "{{ k3s_data_dir }}" + state: absent + become: "{{ k3s_become }}" + +- name: Remove K3S systemd units + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /etc/systemd/system/k3s.service + - /etc/systemd/system/k3s-agent.service + - /etc/systemd/system/k3s.service.env + - /etc/systemd/system/k3s-agent.service.env + become: "{{ k3s_become }}" + notify: Reload systemd + +- name: Remove sysctl config + ansible.builtin.file: + path: /etc/sysctl.d/99-k3s.conf + state: absent + become: "{{ k3s_become }}" + +- name: Remove kernel modules config + ansible.builtin.file: + path: /etc/modules-load.d/k3s.conf + state: absent + become: "{{ k3s_become }}" + +- name: Remove local kubeconfig + ansible.builtin.file: + path: "{{ k3s_kubeconfig_local_path }}" + state: absent + delegate_to: localhost + become: false + run_once: true + +- name: Reload systemd daemon + ansible.builtin.systemd: + daemon_reload: true + become: "{{ k3s_become }}" diff --git a/roles/k3s/tasks/upgrade.yml b/roles/k3s/tasks/upgrade.yml new file mode 100644 index 0000000..e8cd68c --- /dev/null +++ b/roles/k3s/tasks/upgrade.yml @@ -0,0 +1,118 @@ +--- +# Обновление K3S на всех нодах кластера +# Порядок: сначала master, затем workers (по одному) + +- name: Get current K3S version + ansible.builtin.command: k3s --version + register: k3s_current_version + become: "{{ k3s_become }}" + changed_when: false + failed_when: false + +- name: Show current version + ansible.builtin.debug: + msg: "Current: {{ k3s_current_version.stdout | default('not installed') }} → Target: {{ k3s_version }}" + +- name: Skip upgrade if already at target version + ansible.builtin.meta: end_host + when: + - k3s_current_version.rc == 0 + - k3s_version in k3s_current_version.stdout + +# ── Upgrade Master ──────────────────────────────────────────────────────────── +- name: Drain master node before upgrade + ansible.builtin.command: > + k3s kubectl drain {{ inventory_hostname }} + --ignore-daemonsets + --delete-emptydir-data + --timeout=120s + become: "{{ k3s_become }}" + delegate_to: "{{ groups['k3s_master'][0] }}" + changed_when: true + when: inventory_hostname in groups['k3s_master'] + +- name: Upgrade K3S server binary + ansible.builtin.shell: | + set -o pipefail + curl -sfL https://get.k3s.io | \ + INSTALL_K3S_VERSION="{{ k3s_version }}" \ + INSTALL_K3S_EXEC="server" \ + K3S_TOKEN="{{ k3s_token }}" \ + sh - + args: + executable: /bin/bash + become: "{{ k3s_become }}" + when: inventory_hostname in groups['k3s_master'] + +- name: Wait for master to be ready after upgrade + ansible.builtin.command: > + k3s kubectl get node {{ inventory_hostname }} -o jsonpath='{.status.conditions[-1].type}' + become: "{{ k3s_become }}" + register: master_ready + until: master_ready.stdout == "Ready" + retries: 24 + delay: 10 + changed_when: false + when: inventory_hostname in groups['k3s_master'] + +- name: Uncordon master node + ansible.builtin.command: k3s kubectl uncordon {{ inventory_hostname }} + become: "{{ k3s_become }}" + delegate_to: "{{ groups['k3s_master'][0] }}" + changed_when: true + when: inventory_hostname in groups['k3s_master'] + +# ── Upgrade Workers (serial) ────────────────────────────────────────────────── +- name: Drain worker node before upgrade + ansible.builtin.command: > + k3s kubectl drain {{ inventory_hostname }} + --ignore-daemonsets + --delete-emptydir-data + --timeout=180s + become: "{{ k3s_become }}" + delegate_to: "{{ groups['k3s_master'][0] }}" + changed_when: true + when: inventory_hostname in groups['k3s_workers'] + +- name: Upgrade K3S agent binary + ansible.builtin.shell: | + set -o pipefail + curl -sfL https://get.k3s.io | \ + INSTALL_K3S_VERSION="{{ k3s_version }}" \ + INSTALL_K3S_EXEC="agent" \ + K3S_URL="{{ k3s_api_url }}" \ + K3S_TOKEN="{{ hostvars[groups['k3s_master'][0]]['k3s_node_token'] }}" \ + sh - + args: + executable: /bin/bash + become: "{{ k3s_become }}" + when: inventory_hostname in groups['k3s_workers'] + +- name: Wait for worker to rejoin after upgrade + ansible.builtin.command: > + k3s kubectl get node {{ inventory_hostname }} -o jsonpath='{.status.conditions[-1].type}' + become: "{{ k3s_become }}" + delegate_to: "{{ groups['k3s_master'][0] }}" + register: worker_ready + until: worker_ready.stdout == "Ready" + retries: 24 + delay: 10 + changed_when: false + when: inventory_hostname in groups['k3s_workers'] + +- name: Uncordon worker node + ansible.builtin.command: k3s kubectl uncordon {{ inventory_hostname }} + become: "{{ k3s_become }}" + delegate_to: "{{ groups['k3s_master'][0] }}" + changed_when: true + when: inventory_hostname in groups['k3s_workers'] + +- name: Verify new version + ansible.builtin.command: k3s --version + register: k3s_new_version + become: "{{ k3s_become }}" + changed_when: false + +- name: Show upgraded version + ansible.builtin.debug: + msg: "✓ {{ inventory_hostname }} upgraded to: {{ k3s_new_version.stdout }}" diff --git a/roles/k3s/templates/k3s-agent-config.yaml.j2 b/roles/k3s/templates/k3s-agent-config.yaml.j2 new file mode 100644 index 0000000..c41bc7e --- /dev/null +++ b/roles/k3s/templates/k3s-agent-config.yaml.j2 @@ -0,0 +1,19 @@ +# K3S Agent Configuration +# Generated by Ansible — do not edit manually + +server: "{{ k3s_api_url }}" +token: "{{ k3s_node_token }}" + +# Node name +node-name: "{{ inventory_hostname }}" + +{% if ansible_architecture in ['armv7l', 'aarch64'] %} +# Raspberry Pi / ARM — use container runtime args for stability +kubelet-arg: + - "feature-gates=MemoryManager=false" +{% endif %} + +{% if k3s_extra_agent_args %} +# Extra args +{{ k3s_extra_agent_args }} +{% endif %} diff --git a/roles/k3s/templates/k3s-server-config.yaml.j2 b/roles/k3s/templates/k3s-server-config.yaml.j2 new file mode 100644 index 0000000..b296d75 --- /dev/null +++ b/roles/k3s/templates/k3s-server-config.yaml.j2 @@ -0,0 +1,41 @@ +# K3S Server Configuration +# Generated by Ansible — do not edit manually + +token: "{{ k3s_token }}" +cluster-cidr: "{{ k3s_cluster_cidr }}" +service-cidr: "{{ k3s_service_cidr }}" +cluster-dns: "{{ k3s_cluster_dns }}" +{% if k3s_cni | default('flannel') == 'flannel' %} +flannel-backend: "{{ k3s_flannel_backend }}" +{% else %} +flannel-backend: "none" +disable-network-policy: true +{% endif %} + +write-kubeconfig-mode: "0644" + +# HA embedded etcd: первый сервер инициализирует кластер, остальные присоединяются +{% if inventory_hostname == groups['k3s_master'][0] %} +cluster-init: true +{% else %} +server: "https://{{ hostvars[groups['k3s_master'][0]]['ansible_host'] }}:6443" +{% endif %} + +{% if k3s_disable_traefik or k3s_disable_servicelb or k3s_disable_local_storage %} +disable: +{% if k3s_disable_traefik %} + - traefik +{% endif %} +{% if k3s_disable_servicelb %} + - servicelb +{% endif %} +{% if k3s_disable_local_storage %} + - local-storage +{% endif %} +{% endif %} + +{% if k3s_extra_server_args %} +{{ k3s_extra_server_args }} +{% endif %} + +node-name: "{{ inventory_hostname }}" diff --git a/roles/kube-vip/defaults/main.yml b/roles/kube-vip/defaults/main.yml new file mode 100644 index 0000000..a07e5ec --- /dev/null +++ b/roles/kube-vip/defaults/main.yml @@ -0,0 +1,27 @@ +--- +# Версия kube-vip +kube_vip_version: "v0.8.3" + +# VIP — виртуальный IP для control plane (ОБЯЗАТЕЛЬНО задать!) +kube_vip_address: "192.168.1.100" + +# Сетевой интерфейс на master-ноде. +# Оставь пустым для автоопределения (ansible_default_ipv4.interface). +# Переопредели если авто не работает: kube_vip_interface: "eth0" +kube_vip_interface: "" + +# Режим работы: ARP (L2, для большинства домашних сетей) или BGP (L3) +kube_vip_mode: "arp" # arp | bgp + +# Включить kube-vip как LoadBalancer для Services (вместо metalLB) +kube_vip_services_enable: true + +# RBAC манифест +kube_vip_rbac_url: "https://kube-vip.io/manifests/rbac.yaml" + +# Образ kube-vip +kube_vip_image: "ghcr.io/kube-vip/kube-vip" + +# Путь для статического пода +kube_vip_manifest_dir: /var/lib/rancher/k3s/server/manifests +kube_vip_pod_manifest: "{{ kube_vip_manifest_dir }}/kube-vip.yaml" diff --git a/roles/kube-vip/handlers/main.yml b/roles/kube-vip/handlers/main.yml new file mode 100644 index 0000000..58b1d29 --- /dev/null +++ b/roles/kube-vip/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: Restart K3S server + ansible.builtin.systemd: + name: k3s + state: restarted + daemon_reload: true + become: true diff --git a/roles/kube-vip/meta/main.yml b/roles/kube-vip/meta/main.yml new file mode 100644 index 0000000..aec5aba --- /dev/null +++ b/roles/kube-vip/meta/main.yml @@ -0,0 +1,8 @@ +--- +galaxy_info: + author: "your-name" + description: "Deploy kube-vip VIP for K3S control plane and LoadBalancer services" + license: "MIT" + min_ansible_version: "2.12" +dependencies: + - role: k3s diff --git a/roles/kube-vip/tasks/main.yml b/roles/kube-vip/tasks/main.yml new file mode 100644 index 0000000..3e27fb6 --- /dev/null +++ b/roles/kube-vip/tasks/main.yml @@ -0,0 +1,62 @@ +--- +- name: Resolve kube-vip network interface + ansible.builtin.set_fact: + _kube_vip_iface: "{{ kube_vip_interface if kube_vip_interface | length > 0 else ansible_default_ipv4.interface | default('eth0') }}" + +- name: Validate kube_vip_address is set + ansible.builtin.assert: + that: + - kube_vip_address is defined + - kube_vip_address != "" + - kube_vip_address != "192.168.1.100" + fail_msg: > + Задай kube_vip_address в group_vars/all/main.yml! + Например: kube_vip_address: "192.168.1.50" + quiet: true + tags: [kube_vip_validate] + +- name: Ensure manifest directory exists + ansible.builtin.file: + path: "{{ kube_vip_manifest_dir }}" + state: directory + mode: '0755' + become: true + +- name: Deploy kube-vip RBAC manifest + ansible.builtin.get_url: + url: "{{ kube_vip_rbac_url }}" + dest: "{{ kube_vip_manifest_dir }}/kube-vip-rbac.yaml" + mode: '0644' + force: true + become: true + retries: 3 + delay: 5 + +- name: Template kube-vip DaemonSet manifest + ansible.builtin.template: + src: kube-vip-ds.yaml.j2 + dest: "{{ kube_vip_manifest_dir }}/kube-vip.yaml" + mode: '0644' + become: true + notify: Restart K3S server + +- name: Wait for kube-vip pods to be ready + ansible.builtin.command: > + k3s kubectl -n kube-system wait pod + -l app.kubernetes.io/name=kube-vip + --for=condition=Ready + --timeout=120s + become: true + register: kvip_wait + retries: 5 + delay: 10 + until: kvip_wait.rc == 0 + changed_when: false + +- name: Verify VIP is reachable + ansible.builtin.wait_for: + host: "{{ kube_vip_address }}" + port: 6443 + timeout: 60 + delegate_to: localhost + become: false diff --git a/roles/kube-vip/templates/kube-vip-ds.yaml.j2 b/roles/kube-vip/templates/kube-vip-ds.yaml.j2 new file mode 100644 index 0000000..93f05f5 --- /dev/null +++ b/roles/kube-vip/templates/kube-vip-ds.yaml.j2 @@ -0,0 +1,107 @@ +--- +# kube-vip DaemonSet — деплоится как auto-манифест K3S +# Сгенерировано Ansible (roles/kube-vip) +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: kube-vip-ds + namespace: kube-system + labels: + app.kubernetes.io/name: kube-vip + app.kubernetes.io/version: "{{ kube_vip_version }}" +spec: + selector: + matchLabels: + app.kubernetes.io/name: kube-vip + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + app.kubernetes.io/name: kube-vip + app.kubernetes.io/version: "{{ kube_vip_version }}" + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node-role.kubernetes.io/control-plane + operator: Exists + tolerations: + - effect: NoSchedule + operator: Exists + - effect: NoExecute + operator: Exists + hostNetwork: true + serviceAccountName: kube-vip + containers: + - name: kube-vip + image: "{{ kube_vip_image }}:{{ kube_vip_version }}" + imagePullPolicy: IfNotPresent + args: + - manager + env: + - name: vip_arp + value: "{{ 'true' if kube_vip_mode == 'arp' else 'false' }}" + - name: vip_interface + value: "{{ _kube_vip_iface }}" + - name: vip_address + value: "{{ kube_vip_address }}" + - name: port + value: "6443" + - name: vip_cidr + value: "32" + - name: cp_enable + value: "true" + - name: cp_namespace + value: "kube-system" + - name: vip_ddns + value: "false" + - name: svc_enable + value: "{{ 'true' if kube_vip_services_enable else 'false' }}" + - name: svc_leasename + value: "plndr-svcs-lock" + - name: vip_leaderelection + value: "true" + - name: vip_leasename + value: "plndr-cp-lock" + - name: vip_leaseduration + value: "5" + - name: vip_renewdeadline + value: "3" + - name: vip_retryperiod + value: "1" + - name: prometheus_server + value: ":2112" + securityContext: + capabilities: + add: + - NET_ADMIN + - NET_RAW + - SYS_TIME + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + livenessProbe: + httpGet: + path: /healthz + port: 2112 + initialDelaySeconds: 15 + periodSeconds: 20 +{% if kube_vip_mode == 'bgp' %} + - name: bgp_enable + value: "true" + - name: bgp_routerid + value: "{{ ansible_default_ipv4.address }}" + - name: bgp_as + value: "{{ kube_vip_bgp_as | default('65000') }}" + - name: bgp_peeraddress + value: "{{ kube_vip_bgp_peer | default('') }}" + - name: bgp_peeras + value: "{{ kube_vip_bgp_peer_as | default('65000') }}" +{% endif %} diff --git a/roles/nfs-server/defaults/main.yml b/roles/nfs-server/defaults/main.yml new file mode 100644 index 0000000..ae821f0 --- /dev/null +++ b/roles/nfs-server/defaults/main.yml @@ -0,0 +1,21 @@ +--- +# NFS экспорты — список точек монтирования +nfs_exports: + - path: /srv/nfs/k8s + options: "*(rw,sync,no_subtree_check,no_root_squash)" + +# Разрешённая подсеть для NFS (для firewall) +nfs_allowed_network: "192.168.1.0/24" + +# Пакеты NFS сервера +nfs_server_packages: + - nfs-kernel-server + - nfs-common + +# Создавать директории если не существуют +nfs_create_export_dirs: true + +# Права на экспортируемые директории +nfs_export_dir_mode: "0777" +nfs_export_dir_owner: "nobody" +nfs_export_dir_group: "nogroup" diff --git a/roles/nfs-server/handlers/main.yml b/roles/nfs-server/handlers/main.yml new file mode 100644 index 0000000..f7e61cc --- /dev/null +++ b/roles/nfs-server/handlers/main.yml @@ -0,0 +1,11 @@ +--- +- name: Re-export NFS shares + ansible.builtin.command: exportfs -ra + become: true + changed_when: true + +- name: Restart NFS server + ansible.builtin.systemd: + name: nfs-kernel-server + state: restarted + become: true diff --git a/roles/nfs-server/meta/main.yml b/roles/nfs-server/meta/main.yml new file mode 100644 index 0000000..6383421 --- /dev/null +++ b/roles/nfs-server/meta/main.yml @@ -0,0 +1,7 @@ +--- +galaxy_info: + author: "your-name" + description: "Configure NFS server for Kubernetes persistent storage" + license: "MIT" + min_ansible_version: "2.12" +dependencies: [] diff --git a/roles/nfs-server/tasks/main.yml b/roles/nfs-server/tasks/main.yml new file mode 100644 index 0000000..227c2f1 --- /dev/null +++ b/roles/nfs-server/tasks/main.yml @@ -0,0 +1,58 @@ +--- +- name: Install NFS server packages + ansible.builtin.apt: + name: "{{ nfs_server_packages }}" + state: present + update_cache: true + become: true + +- name: Create NFS export directories + ansible.builtin.file: + path: "{{ item.path }}" + state: directory + mode: "{{ nfs_export_dir_mode }}" + owner: "{{ nfs_export_dir_owner }}" + group: "{{ nfs_export_dir_group }}" + loop: "{{ nfs_exports }}" + become: true + when: nfs_create_export_dirs + +- name: Configure /etc/exports + ansible.builtin.template: + src: exports.j2 + dest: /etc/exports + mode: '0644' + backup: true + become: true + notify: + - Re-export NFS shares + - Restart NFS server + +- name: Enable and start NFS server + ansible.builtin.systemd: + name: nfs-kernel-server + enabled: true + state: started + become: true + +- name: Allow NFS through UFW (if active) + community.general.ufw: + rule: allow + src: "{{ nfs_allowed_network }}" + port: "{{ item }}" + proto: tcp + loop: + - "2049" # NFS + - "111" # RPC portmapper + become: true + failed_when: false # UFW может быть не установлен + +- name: Verify NFS exports are active + ansible.builtin.command: exportfs -v + register: nfs_exportfs + become: true + changed_when: false + +- name: Show active NFS exports + ansible.builtin.debug: + msg: "{{ nfs_exportfs.stdout_lines }}" diff --git a/roles/nfs-server/templates/exports.j2 b/roles/nfs-server/templates/exports.j2 new file mode 100644 index 0000000..c9ea79b --- /dev/null +++ b/roles/nfs-server/templates/exports.j2 @@ -0,0 +1,9 @@ +# /etc/exports — управляется Ansible (roles/nfs-server) +# Изменения вручную будут перезаписаны! +# +# Формат: <директория> <опции> +# Документация: man exports + +{% for export in nfs_exports %} +{{ export.path }} {{ export.options }} +{% endfor %} diff --git a/roles/prometheus-stack/defaults/main.yml b/roles/prometheus-stack/defaults/main.yml new file mode 100644 index 0000000..e21a67e --- /dev/null +++ b/roles/prometheus-stack/defaults/main.yml @@ -0,0 +1,63 @@ +--- +# Включить установку kube-prometheus-stack (false = пропустить) +prometheus_stack_enabled: false + +prometheus_stack_version: "60.3.0" # Helm chart version +prometheus_stack_namespace: "monitoring" +prometheus_stack_release_name: "prom" + +prometheus_stack_chart_repo: "https://prometheus-community.github.io/helm-charts" +prometheus_stack_chart_name: "kube-prometheus-stack" + +# Grafana +prometheus_grafana_enabled: true + +# Логин и пароль администратора Grafana. +# Рекомендуется задавать через Ansible Vault: +# group_vars/all/vault.yml: +# vault_grafana_user: "admin" +# vault_grafana_password: "ваш-пароль" +grafana_admin_user: "{{ vault_grafana_user | default('admin') }}" +prometheus_grafana_admin_password: "{{ vault_grafana_password | default('admin') }}" + +prometheus_grafana_ingress_enabled: false +prometheus_grafana_ingress_host: "grafana.local" +prometheus_grafana_ingress_class: "nginx" + +# Prometheus +prometheus_retention_days: 7 +prometheus_storage_size: "10Gi" # Размер PVC для данных Prometheus +prometheus_storage_class: "" # "" = использовать default StorageClass (nfs-client) + +# Grafana PVC +grafana_storage_enabled: true +grafana_storage_size: "5Gi" # Размер PVC для дашбордов и плагинов Grafana +grafana_storage_class: "" # "" = использовать default StorageClass + +# Alertmanager +prometheus_alertmanager_enabled: true +prometheus_alertmanager_storage_size: "2Gi" + +# Node exporter (метрики хостов) +prometheus_node_exporter_enabled: true + +# kube-state-metrics +prometheus_kube_state_metrics_enabled: true + +# Ресурсы Prometheus +prometheus_resources: + requests: + cpu: 200m + memory: 512Mi + limits: + cpu: 1000m + memory: 2Gi + +# Ресурсы Grafana +grafana_resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 300m + memory: 256Mi diff --git a/roles/prometheus-stack/meta/main.yml b/roles/prometheus-stack/meta/main.yml new file mode 100644 index 0000000..7df1321 --- /dev/null +++ b/roles/prometheus-stack/meta/main.yml @@ -0,0 +1,6 @@ +--- +galaxy_info: + role_name: prometheus-stack + description: Deploy kube-prometheus-stack (Prometheus + Grafana + Alertmanager) via Helm on K3S + min_ansible_version: "2.14" +dependencies: [] diff --git a/roles/prometheus-stack/molecule/default/converge.yml b/roles/prometheus-stack/molecule/default/converge.yml new file mode 100644 index 0000000..97861da --- /dev/null +++ b/roles/prometheus-stack/molecule/default/converge.yml @@ -0,0 +1,54 @@ +--- +- name: Converge — prometheus-stack template tests + hosts: all + become: false + gather_facts: false + + vars: + prometheus_stack_enabled: true + prometheus_stack_namespace: "monitoring" + prometheus_stack_release_name: "prom" + prometheus_stack_chart_name: "kube-prometheus-stack" + + prometheus_grafana_enabled: true + grafana_admin_user: "admin" + prometheus_grafana_admin_password: "molecule-test-pass" + prometheus_grafana_ingress_enabled: false + prometheus_grafana_ingress_host: "grafana.local" + prometheus_grafana_ingress_class: "nginx" + + grafana_storage_enabled: true + grafana_storage_size: "5Gi" + grafana_storage_class: "" + + prometheus_retention_days: 7 + prometheus_storage_size: "10Gi" + prometheus_storage_class: "" + + prometheus_alertmanager_enabled: true + prometheus_alertmanager_storage_size: "2Gi" + prometheus_node_exporter_enabled: true + prometheus_kube_state_metrics_enabled: true + + prometheus_resources: + requests: + cpu: 200m + memory: 512Mi + limits: + cpu: 1000m + memory: 2Gi + + grafana_resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 300m + memory: 256Mi + + tasks: + - name: Render kube-prometheus-stack Helm values + ansible.builtin.template: + src: "{{ playbook_dir }}/../../templates/prometheus-stack-values.yaml.j2" + dest: /tmp/prometheus-stack-values.yaml + mode: '0644' diff --git a/roles/prometheus-stack/molecule/default/molecule.yml b/roles/prometheus-stack/molecule/default/molecule.yml new file mode 100644 index 0000000..4c8a947 --- /dev/null +++ b/roles/prometheus-stack/molecule/default/molecule.yml @@ -0,0 +1,25 @@ +--- +driver: + name: docker + +platforms: + - name: prom-test + image: geerlingguy/docker-ubuntu2204-ansible:latest + pre_build_image: true + +provisioner: + name: ansible + playbooks: + converge: converge.yml + verify: verify.yml + config_options: + defaults: + interpreter_python: auto_silent + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint diff --git a/roles/prometheus-stack/molecule/default/verify.yml b/roles/prometheus-stack/molecule/default/verify.yml new file mode 100644 index 0000000..00b375e --- /dev/null +++ b/roles/prometheus-stack/molecule/default/verify.yml @@ -0,0 +1,80 @@ +--- +- name: Verify — prometheus-stack templates + hosts: all + become: false + gather_facts: false + + tasks: + - name: Read rendered Helm values + ansible.builtin.slurp: + src: /tmp/prometheus-stack-values.yaml + register: values_raw + + - name: Parse YAML + ansible.builtin.set_fact: + v: "{{ values_raw.content | b64decode | from_yaml }}" + + # ── Grafana ───────────────────────────────────────────────────────────────── + - name: Assert grafana block exists + ansible.builtin.assert: + that: v.grafana is defined + fail_msg: "Блок grafana отсутствует в values" + + - name: Assert grafana adminUser + ansible.builtin.assert: + that: v.grafana.adminUser == 'admin' + fail_msg: "grafana.adminUser неверный: {{ v.grafana.adminUser }}" + + - name: Assert grafana adminPassword is set + ansible.builtin.assert: + that: v.grafana.adminPassword | length > 0 + fail_msg: "grafana.adminPassword не задан" + + - name: Assert grafana persistence is enabled + ansible.builtin.assert: + that: + - v.grafana.persistence is defined + - v.grafana.persistence.enabled == true + - v.grafana.persistence.size == '5Gi' + fail_msg: "grafana.persistence настроена неверно: {{ v.grafana.persistence }}" + + # ── Prometheus ────────────────────────────────────────────────────────────── + - name: Assert prometheus block exists + ansible.builtin.assert: + that: v.prometheus.prometheusSpec is defined + fail_msg: "Блок prometheus.prometheusSpec отсутствует" + + - name: Assert prometheus retention + ansible.builtin.assert: + that: v.prometheus.prometheusSpec.retention == '7d' + fail_msg: "Неверный retention: {{ v.prometheus.prometheusSpec.retention }}" + + - name: Assert prometheus PVC storage + ansible.builtin.assert: + that: + - v.prometheus.prometheusSpec.storageSpec is defined + - v.prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage == '10Gi' + fail_msg: "Prometheus PVC настроен неверно" + + # ── Alertmanager ──────────────────────────────────────────────────────────── + - name: Assert alertmanager is enabled + ansible.builtin.assert: + that: v.alertmanager.enabled == true + fail_msg: "alertmanager.enabled должен быть true" + + - name: Assert alertmanager storage + ansible.builtin.assert: + that: + - v.alertmanager.alertmanagerSpec.storage is defined + - v.alertmanager.alertmanagerSpec.storage.volumeClaimTemplate.spec.resources.requests.storage == '2Gi' + fail_msg: "Alertmanager PVC настроен неверно" + + # ── Node Exporter & kube-state-metrics ────────────────────────────────────── + - name: Assert nodeExporter enabled + ansible.builtin.assert: + that: v.nodeExporter.enabled == true + fail_msg: "nodeExporter.enabled должен быть true" + + - name: Summary + ansible.builtin.debug: + msg: "Все проверки prometheus-stack прошли успешно" diff --git a/roles/prometheus-stack/tasks/main.yml b/roles/prometheus-stack/tasks/main.yml new file mode 100644 index 0000000..6593324 --- /dev/null +++ b/roles/prometheus-stack/tasks/main.yml @@ -0,0 +1,112 @@ +--- +- name: Prometheus Stack — skip if not enabled + ansible.builtin.debug: + msg: "kube-prometheus-stack отключён (prometheus_stack_enabled: false). Пропускаем." + when: not prometheus_stack_enabled + run_once: true + +- name: Prometheus Stack — install + when: prometheus_stack_enabled + block: + - name: Install Helm (if needed) + ansible.builtin.include_tasks: "{{ playbook_dir }}/../roles/csi-nfs/tasks/install_helm.yml" + + - name: Add prometheus-community Helm repo + kubernetes.core.helm_repository: + name: prometheus-community + repo_url: "{{ prometheus_stack_chart_repo }}" + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + environment: + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + + - name: Update Helm repos + ansible.builtin.command: helm repo update + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + changed_when: false + environment: + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + + - name: Create monitoring namespace + ansible.builtin.command: > + k3s kubectl create namespace {{ prometheus_stack_namespace }} + --dry-run=client -o yaml | k3s kubectl apply -f - + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + changed_when: false + + - name: Template kube-prometheus-stack values + ansible.builtin.template: + src: prometheus-stack-values.yaml.j2 + dest: /tmp/prometheus-stack-values.yaml + mode: '0644' + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + + - name: Deploy kube-prometheus-stack via Helm + kubernetes.core.helm: + name: "{{ prometheus_stack_release_name }}" + chart_ref: "prometheus-community/{{ prometheus_stack_chart_name }}" + chart_version: "{{ prometheus_stack_version }}" + release_namespace: "{{ prometheus_stack_namespace }}" + create_namespace: true + wait: true + timeout: "10m0s" + values_files: + - /tmp/prometheus-stack-values.yaml + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + environment: + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + + - name: Wait for Prometheus to be ready + ansible.builtin.command: > + k3s kubectl -n {{ prometheus_stack_namespace }} + rollout status deployment/{{ prometheus_stack_release_name }}-grafana + --timeout=180s + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + register: grafana_ready + changed_when: false + retries: 3 + delay: 15 + until: grafana_ready.rc == 0 + + - name: Get Grafana admin password (from secret) + ansible.builtin.command: > + k3s kubectl -n {{ prometheus_stack_namespace }} + get secret {{ prometheus_stack_release_name }}-grafana + -o jsonpath="{.data.admin-password}" + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + register: grafana_secret + changed_when: false + + - name: Show Grafana access info + ansible.builtin.debug: + msg: + - "Grafana URL: http://{{ hostvars[groups['k3s_master'][0]]['ansible_host'] }}:3000 (NodePort) или через Ingress" + - "Admin user: admin" + - "Admin password: {{ grafana_secret.stdout | b64decode }}" + run_once: true + + - name: Show monitoring namespace pods + ansible.builtin.command: > + k3s kubectl -n {{ prometheus_stack_namespace }} get pods + become: true + delegate_to: "{{ groups['k3s_master'][0] }}" + run_once: true + register: prom_pods + changed_when: false + + - name: Monitoring pods + ansible.builtin.debug: + msg: "{{ prom_pods.stdout_lines }}" + run_once: true diff --git a/roles/prometheus-stack/templates/prometheus-stack-values.yaml.j2 b/roles/prometheus-stack/templates/prometheus-stack-values.yaml.j2 new file mode 100644 index 0000000..2e570d7 --- /dev/null +++ b/roles/prometheus-stack/templates/prometheus-stack-values.yaml.j2 @@ -0,0 +1,119 @@ +## kube-prometheus-stack Helm values +## Управляется Ansible (roles/prometheus-stack) + +grafana: + enabled: {{ prometheus_grafana_enabled | lower }} + adminUser: "{{ grafana_admin_user }}" + adminPassword: "{{ prometheus_grafana_admin_password }}" + + resources: + requests: + cpu: "{{ grafana_resources.requests.cpu }}" + memory: "{{ grafana_resources.requests.memory }}" + limits: + cpu: "{{ grafana_resources.limits.cpu }}" + memory: "{{ grafana_resources.limits.memory }}" + + tolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + +{% if prometheus_grafana_ingress_enabled %} + ingress: + enabled: true + ingressClassName: "{{ prometheus_grafana_ingress_class }}" + hosts: + - "{{ prometheus_grafana_ingress_host }}" + paths: + - / +{% else %} + service: + type: NodePort + nodePort: 32000 +{% endif %} + + # Готовые дашборды + defaultDashboardsEnabled: true + defaultDashboardsTimezone: utc + + persistence: + enabled: {{ grafana_storage_enabled | lower }} + type: pvc + accessModes: + - ReadWriteOnce + size: "{{ grafana_storage_size }}" +{% if grafana_storage_class %} + storageClassName: "{{ grafana_storage_class }}" +{% endif %} + +prometheus: + prometheusSpec: + retention: "{{ prometheus_retention_days }}d" + + resources: + requests: + cpu: "{{ prometheus_resources.requests.cpu }}" + memory: "{{ prometheus_resources.requests.memory }}" + limits: + cpu: "{{ prometheus_resources.limits.cpu }}" + memory: "{{ prometheus_resources.limits.memory }}" + + tolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + + storageSpec: + volumeClaimTemplate: + spec: +{% if prometheus_storage_class %} + storageClassName: "{{ prometheus_storage_class }}" +{% endif %} + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: "{{ prometheus_storage_size }}" + + # Собирать метрики со всех namespaces + serviceMonitorSelectorNilUsesHelmValues: false + podMonitorSelectorNilUsesHelmValues: false + ruleSelectorNilUsesHelmValues: false + +alertmanager: + enabled: {{ prometheus_alertmanager_enabled | lower }} + alertmanagerSpec: + tolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + storage: + volumeClaimTemplate: + spec: +{% if prometheus_storage_class %} + storageClassName: "{{ prometheus_storage_class }}" +{% endif %} + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: "{{ prometheus_alertmanager_storage_size }}" + +nodeExporter: + enabled: {{ prometheus_node_exporter_enabled | lower }} + # DaemonSet — запускается на всех нодах включая мастера + tolerations: + - operator: "Exists" + +kube-state-metrics: + enabled: {{ prometheus_kube_state_metrics_enabled | lower }} + tolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" + +# Не устанавливать дополнительный Prometheus Operator если уже есть +prometheusOperator: + tolerations: + - key: "node-role.kubernetes.io/control-plane" + operator: "Exists" + effect: "NoSchedule" diff --git a/site.yml b/site.yml new file mode 100644 index 0000000..b80464c --- /dev/null +++ b/site.yml @@ -0,0 +1,129 @@ +--- +# ───────────────────────────────────────────────────────────────────────────── +# K3S Full Stack Playbook +# Порядок установки: +# 1. K3S cluster (master → workers) +# 2. kube-vip (VIP для control plane + LoadBalancer) +# 3. NFS Server (на master или отдельном хосте) +# 4. CSI NFS Driver (StorageClass для PVC) +# 5. ingress-nginx (Ingress controller через Helm) +# +# Запуск: ansible-playbook site.yml --ask-vault-pass +# Только отдельный компонент: ansible-playbook site.yml --tags kube_vip +# ───────────────────────────────────────────────────────────────────────────── + +# ── 1. K3S Cluster ──────────────────────────────────────────────────────────── +# serial: 1 — master01 запускается первым (cluster-init), остальные присоединяются +- name: Install K3S cluster (HA embedded etcd) + hosts: k3s_cluster + gather_facts: true + become: true + serial: 1 + tags: [k3s] + roles: + - role: k3s + +# ── 2. CNI (calico/cilium; при flannel — пропускается) ─────────────────────── +- name: Deploy CNI plugin + hosts: "{{ groups['k3s_master'][0] }}" + gather_facts: false + become: true + tags: [cni] + roles: + - role: cni + +# ── 3. kube-vip ─────────────────────────────────────────────────────────────── +- name: Deploy kube-vip (VIP + LoadBalancer) + hosts: k3s_master + gather_facts: true + become: true + tags: [kube_vip] + roles: + - role: kube-vip + +# ── 4. NFS Server ───────────────────────────────────────────────────────────── +- name: Configure NFS server + hosts: nfs_server + gather_facts: true + become: true + tags: [nfs, nfs_server] + roles: + - role: nfs-server + +# ── 5. NFS client on all K3S nodes + CSI Driver ─────────────────────────────── +- name: Deploy CSI NFS Driver + hosts: k3s_cluster + gather_facts: true + become: true + tags: [nfs, csi_nfs] + roles: + - role: csi-nfs + +# ── 6. ingress-nginx ────────────────────────────────────────────────────────── +- name: Deploy ingress-nginx + hosts: k3s_cluster + gather_facts: true + become: true + tags: [ingress, ingress_nginx] + roles: + - role: ingress-nginx + +# ── 6. Istio (опционально: istio_enabled: true) ─────────────────────────────── +- name: Deploy Istio service mesh + hosts: "{{ groups['k3s_master'][0] }}" + gather_facts: false + become: true + tags: [istio] + roles: + - role: istio + +# ── 7. kube-prometheus-stack (опционально: prometheus_stack_enabled: true) ───── +- name: Deploy kube-prometheus-stack + hosts: "{{ groups['k3s_master'][0] }}" + gather_facts: false + become: true + tags: [monitoring, prometheus] + roles: + - role: prometheus-stack + +# ── Verify full stack ───────────────────────────────────────────────────────── +- name: Verify full stack + hosts: k3s_master + gather_facts: false + become: true + tags: [verify] + tasks: + - name: Nodes + ansible.builtin.command: k3s kubectl get nodes -o wide + register: nodes + changed_when: false + - ansible.builtin.debug: + msg: "{{ nodes.stdout_lines }}" + + - name: All pods + ansible.builtin.command: k3s kubectl get pods -A + register: pods + changed_when: false + - ansible.builtin.debug: + msg: "{{ pods.stdout_lines }}" + + - name: Services with External IPs + ansible.builtin.command: k3s kubectl get svc -A + register: svcs + changed_when: false + - ansible.builtin.debug: + msg: "{{ svcs.stdout_lines }}" + + - name: StorageClasses + ansible.builtin.command: k3s kubectl get storageclass + register: sc + changed_when: false + - ansible.builtin.debug: + msg: "{{ sc.stdout_lines }}" + + - name: IngressClasses + ansible.builtin.command: k3s kubectl get ingressclass + register: ic + changed_when: false + - ansible.builtin.debug: + msg: "{{ ic.stdout_lines }}" diff --git a/uninstall.yml b/uninstall.yml new file mode 100644 index 0000000..a98b12d --- /dev/null +++ b/uninstall.yml @@ -0,0 +1,101 @@ +--- +# Полное удаление всего стека +# Порядок: обратный установке +# Запуск: ansible-playbook uninstall.yml + +- name: Confirm uninstall + hosts: localhost + gather_facts: false + tasks: + - name: Ask for confirmation + ansible.builtin.pause: + prompt: | + ⚠️ ВНИМАНИЕ! Будут удалены: ingress-nginx, CSI NFS, kube-vip, K3S и ВСЕ данные. + Введите 'yes' для подтверждения + register: confirm + when: confirm_uninstall is not defined + + - name: Abort if not confirmed + ansible.builtin.fail: + msg: "Отменено." + when: + - confirm_uninstall is not defined + - confirm.user_input != 'yes' + +- name: Remove ingress-nginx + hosts: k3s_master + gather_facts: false + become: true + tags: [ingress_nginx] + tasks: + - name: Uninstall ingress-nginx Helm release + ansible.builtin.command: > + helm uninstall ingress-nginx -n ingress-nginx + failed_when: false + changed_when: true + environment: + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + + - name: Delete ingress-nginx namespace + ansible.builtin.command: k3s kubectl delete namespace ingress-nginx --ignore-not-found + changed_when: true + +- name: Remove CSI NFS Driver + hosts: k3s_master + gather_facts: false + become: true + tags: [csi_nfs] + tasks: + - name: Uninstall CSI NFS Helm release + ansible.builtin.command: > + helm uninstall csi-driver-nfs -n kube-system + failed_when: false + changed_when: true + environment: + KUBECONFIG: /etc/rancher/k3s/k3s.yaml + + - name: Delete NFS StorageClass + ansible.builtin.command: > + k3s kubectl delete storageclass nfs-client --ignore-not-found + changed_when: true + +- name: Remove kube-vip + hosts: k3s_master + gather_facts: false + become: true + tags: [kube_vip] + tasks: + - name: Remove kube-vip manifests + ansible.builtin.file: + path: "{{ item }}" + state: absent + loop: + - /var/lib/rancher/k3s/server/manifests/kube-vip.yaml + - /var/lib/rancher/k3s/server/manifests/kube-vip-rbac.yaml + +- name: Uninstall K3S workers + hosts: k3s_workers + gather_facts: true + become: true + tags: [k3s] + tasks: + - ansible.builtin.include_role: + name: k3s + tasks_from: uninstall + +- name: Uninstall K3S master + hosts: k3s_master + gather_facts: true + become: true + tags: [k3s] + tasks: + - ansible.builtin.include_role: + name: k3s + tasks_from: uninstall + +- name: Done + hosts: localhost + gather_facts: false + tasks: + - ansible.builtin.debug: + msg: "✓ Весь стек удалён." diff --git a/upgrade.yml b/upgrade.yml new file mode 100644 index 0000000..698dba0 --- /dev/null +++ b/upgrade.yml @@ -0,0 +1,40 @@ +--- +# Плейбук обновления K3S +# Workers обновляются по одному (serial: 1) чтобы не потерять кворум +# +# Запуск: ansible-playbook upgrade.yml -e k3s_version=v1.30.0+k3s1 + +- name: Upgrade K3S master + hosts: k3s_master + gather_facts: true + become: true + tasks: + - name: Upgrade K3S server + ansible.builtin.include_role: + name: k3s + tasks_from: upgrade + +- name: Upgrade K3S workers (one by one) + hosts: k3s_workers + gather_facts: true + become: true + serial: 1 # ← обновляем по одной ноде за раз + tasks: + - name: Upgrade K3S agent + ansible.builtin.include_role: + name: k3s + tasks_from: upgrade + +- name: Verify cluster after upgrade + hosts: k3s_master + gather_facts: false + become: true + tasks: + - name: Final node status + ansible.builtin.command: k3s kubectl get nodes -o wide + register: final_status + changed_when: false + + - name: Show final cluster state + ansible.builtin.debug: + msg: "{{ final_status.stdout_lines }}"