From 095b276cb35198f7d288449e2b6b168c9c09ed56 Mon Sep 17 00:00:00 2001 From: Sergey Antropoff Date: Fri, 17 Apr 2026 08:37:27 +0300 Subject: [PATCH] first commit --- .env.example | 35 + .gitignore | 41 + .yamllint.yml | 27 + Dockerfile | 76 + Makefile | 326 ++++ README.md | 1401 +++++++++++++++++ ansible.cfg | 16 + docker-compose.yml | 60 + docker/entrypoint.sh | 286 ++++ group_vars/all/main.yml | 125 ++ group_vars/all/vault.yml.example | 14 + healthcheck.yml | 13 + host_vars/master01/main.yml | 16 + host_vars/rpi01/main.yml | 20 + host_vars/worker01/main.yml | 13 + inventory/hosts.ini | 17 + requirements-python.txt | 28 + requirements.yml | 8 + roles/cni/defaults/main.yml | 32 + roles/cni/tasks/calico.yml | 36 + roles/cni/tasks/cilium.yml | 36 + roles/cni/tasks/main.yml | 12 + .../cni/templates/calico-installation.yaml.j2 | 13 + roles/csi-nfs/defaults/main.yml | 29 + roles/csi-nfs/meta/main.yml | 8 + roles/csi-nfs/tasks/install_helm.yml | 27 + roles/csi-nfs/tasks/main.yml | 102 ++ roles/csi-nfs/templates/storageclass.yaml.j2 | 23 + roles/ingress-nginx/defaults/main.yml | 48 + roles/ingress-nginx/handlers/main.yml | 8 + roles/ingress-nginx/meta/main.yml | 8 + roles/ingress-nginx/tasks/main.yml | 103 ++ .../templates/ingress-nginx-values.yaml.j2 | 96 ++ roles/istio/defaults/main.yml | 63 + roles/istio/meta/main.yml | 6 + roles/istio/molecule/default/converge.yml | 76 + roles/istio/molecule/default/molecule.yml | 25 + roles/istio/molecule/default/verify.yml | 107 ++ roles/istio/tasks/main.yml | 274 ++++ roles/istio/templates/istiod-values.yaml.j2 | 36 + .../templates/kiali-token-secret.yaml.j2 | 10 + roles/istio/templates/kiali-values.yaml.j2 | 64 + .../templates/peer-authentication.yaml.j2 | 9 + roles/k3s/defaults/main.yml | 19 + roles/k3s/handlers/main.yml | 21 + roles/k3s/meta/main.yml | 14 + roles/k3s/molecule/default/converge.yml | 56 + roles/k3s/molecule/default/molecule.yml | 38 + roles/k3s/molecule/default/prepare.yml | 28 + roles/k3s/molecule/default/verify.yml | 104 ++ roles/k3s/tasks/healthcheck.yml | 73 + roles/k3s/tasks/install_agent.yml | 51 + roles/k3s/tasks/install_server.yml | 61 + roles/k3s/tasks/kubeconfig.yml | 27 + roles/k3s/tasks/main.yml | 21 + roles/k3s/tasks/node_config.yml | 18 + roles/k3s/tasks/prereqs.yml | 67 + roles/k3s/tasks/rpi_cgroups.yml | 47 + roles/k3s/tasks/uninstall.yml | 89 ++ roles/k3s/tasks/upgrade.yml | 118 ++ roles/k3s/templates/k3s-agent-config.yaml.j2 | 19 + roles/k3s/templates/k3s-server-config.yaml.j2 | 41 + roles/kube-vip/defaults/main.yml | 27 + roles/kube-vip/handlers/main.yml | 7 + roles/kube-vip/meta/main.yml | 8 + roles/kube-vip/tasks/main.yml | 62 + roles/kube-vip/templates/kube-vip-ds.yaml.j2 | 107 ++ roles/nfs-server/defaults/main.yml | 21 + roles/nfs-server/handlers/main.yml | 11 + roles/nfs-server/meta/main.yml | 7 + roles/nfs-server/tasks/main.yml | 58 + roles/nfs-server/templates/exports.j2 | 9 + roles/prometheus-stack/defaults/main.yml | 63 + roles/prometheus-stack/meta/main.yml | 6 + .../molecule/default/converge.yml | 54 + .../molecule/default/molecule.yml | 25 + .../molecule/default/verify.yml | 80 + roles/prometheus-stack/tasks/main.yml | 112 ++ .../templates/prometheus-stack-values.yaml.j2 | 119 ++ site.yml | 129 ++ uninstall.yml | 101 ++ upgrade.yml | 40 + 82 files changed, 5731 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .yamllint.yml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 ansible.cfg create mode 100644 docker-compose.yml create mode 100644 docker/entrypoint.sh create mode 100644 group_vars/all/main.yml create mode 100644 group_vars/all/vault.yml.example create mode 100644 healthcheck.yml create mode 100644 host_vars/master01/main.yml create mode 100644 host_vars/rpi01/main.yml create mode 100644 host_vars/worker01/main.yml create mode 100644 inventory/hosts.ini create mode 100644 requirements-python.txt create mode 100644 requirements.yml create mode 100644 roles/cni/defaults/main.yml create mode 100644 roles/cni/tasks/calico.yml create mode 100644 roles/cni/tasks/cilium.yml create mode 100644 roles/cni/tasks/main.yml create mode 100644 roles/cni/templates/calico-installation.yaml.j2 create mode 100644 roles/csi-nfs/defaults/main.yml create mode 100644 roles/csi-nfs/meta/main.yml create mode 100644 roles/csi-nfs/tasks/install_helm.yml create mode 100644 roles/csi-nfs/tasks/main.yml create mode 100644 roles/csi-nfs/templates/storageclass.yaml.j2 create mode 100644 roles/ingress-nginx/defaults/main.yml create mode 100644 roles/ingress-nginx/handlers/main.yml create mode 100644 roles/ingress-nginx/meta/main.yml create mode 100644 roles/ingress-nginx/tasks/main.yml create mode 100644 roles/ingress-nginx/templates/ingress-nginx-values.yaml.j2 create mode 100644 roles/istio/defaults/main.yml create mode 100644 roles/istio/meta/main.yml create mode 100644 roles/istio/molecule/default/converge.yml create mode 100644 roles/istio/molecule/default/molecule.yml create mode 100644 roles/istio/molecule/default/verify.yml create mode 100644 roles/istio/tasks/main.yml create mode 100644 roles/istio/templates/istiod-values.yaml.j2 create mode 100644 roles/istio/templates/kiali-token-secret.yaml.j2 create mode 100644 roles/istio/templates/kiali-values.yaml.j2 create mode 100644 roles/istio/templates/peer-authentication.yaml.j2 create mode 100644 roles/k3s/defaults/main.yml create mode 100644 roles/k3s/handlers/main.yml create mode 100644 roles/k3s/meta/main.yml create mode 100644 roles/k3s/molecule/default/converge.yml create mode 100644 roles/k3s/molecule/default/molecule.yml create mode 100644 roles/k3s/molecule/default/prepare.yml create mode 100644 roles/k3s/molecule/default/verify.yml create mode 100644 roles/k3s/tasks/healthcheck.yml create mode 100644 roles/k3s/tasks/install_agent.yml create mode 100644 roles/k3s/tasks/install_server.yml create mode 100644 roles/k3s/tasks/kubeconfig.yml create mode 100644 roles/k3s/tasks/main.yml create mode 100644 roles/k3s/tasks/node_config.yml create mode 100644 roles/k3s/tasks/prereqs.yml create mode 100644 roles/k3s/tasks/rpi_cgroups.yml create mode 100644 roles/k3s/tasks/uninstall.yml create mode 100644 roles/k3s/tasks/upgrade.yml create mode 100644 roles/k3s/templates/k3s-agent-config.yaml.j2 create mode 100644 roles/k3s/templates/k3s-server-config.yaml.j2 create mode 100644 roles/kube-vip/defaults/main.yml create mode 100644 roles/kube-vip/handlers/main.yml create mode 100644 roles/kube-vip/meta/main.yml create mode 100644 roles/kube-vip/tasks/main.yml create mode 100644 roles/kube-vip/templates/kube-vip-ds.yaml.j2 create mode 100644 roles/nfs-server/defaults/main.yml create mode 100644 roles/nfs-server/handlers/main.yml create mode 100644 roles/nfs-server/meta/main.yml create mode 100644 roles/nfs-server/tasks/main.yml create mode 100644 roles/nfs-server/templates/exports.j2 create mode 100644 roles/prometheus-stack/defaults/main.yml create mode 100644 roles/prometheus-stack/meta/main.yml create mode 100644 roles/prometheus-stack/molecule/default/converge.yml create mode 100644 roles/prometheus-stack/molecule/default/molecule.yml create mode 100644 roles/prometheus-stack/molecule/default/verify.yml create mode 100644 roles/prometheus-stack/tasks/main.yml create mode 100644 roles/prometheus-stack/templates/prometheus-stack-values.yaml.j2 create mode 100644 site.yml create mode 100644 uninstall.yml create mode 100644 upgrade.yml 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 }}"