initial commit from gigacoms
This commit is contained in:
parent
4142aaea60
commit
a82c1ca025
155 changed files with 13549 additions and 1 deletions
39
.ansible-lint
Normal file
39
.ansible-lint
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
profile: min
|
||||
|
||||
exclude_paths:
|
||||
- collections/
|
||||
- .ansible/
|
||||
|
||||
skip_list:
|
||||
# Task names follow the project's documented "Category | Action" convention
|
||||
# (see CLAUDE.md), which intentionally starts with a lowercase category (e.g.
|
||||
# "kubectl | Download binary"). Renaming ~50 tasks repo-wide would break that
|
||||
# convention for no functional benefit.
|
||||
- name[casing]
|
||||
|
||||
# Long template expressions in JSON Patch paths are more readable without
|
||||
# extra spaces. The ansible-lint var-spacing rule is overly strict for this
|
||||
# domain-specific syntax.
|
||||
- var-spacing
|
||||
|
||||
# Vars are named after their role's domain concept (e.g. k8s_version,
|
||||
# longhorn_disks) and are shared/read across roles, defaults, group_vars and
|
||||
# templates. Enforcing a role-name prefix here would require a repo-wide,
|
||||
# high-risk rename touching every layer that references these variables.
|
||||
- var-naming[no-role-prefix]
|
||||
|
||||
# Several tasks intentionally run shell/command modules whose "changed"
|
||||
# state is either always true (idempotent by design, e.g. helm upgrade
|
||||
# --install) or checked via a separate registered/failed_when guard rather
|
||||
# than changed_when.
|
||||
- no-changed-when
|
||||
|
||||
# A few changed-triggered tasks (e.g. Traefik rollout restart, GitLab fleet
|
||||
# repo commit/push) are deliberately inline rather than handlers, since they
|
||||
# need to run mid-playbook with immediate ordering guarantees.
|
||||
- no-handler
|
||||
|
||||
# Some Helm values/service description lines exceed 160 chars and are not
|
||||
# worth artificially wrapping.
|
||||
- yaml[line-length]
|
||||
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
LOCAL_RUN.md
|
||||
collections/
|
||||
*.retry
|
||||
/tmp/
|
||||
*.log
|
||||
host_facts/
|
||||
public/
|
||||
.ansible/
|
||||
.claude/
|
||||
.mcp.json
|
||||
/tmp/ansible_facts_cache/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
308
.gitlab-ci.yml
Normal file
308
.gitlab-ci.yml
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
# GitLab CI pipeline for Kubernetes infrastructure (Ansible)
|
||||
#
|
||||
# Required GitLab CI/CD Variables (Settings → CI/CD → Variables):
|
||||
# ANSIBLE_SSH_USER — SSH username for target servers
|
||||
# ANSIBLE_SSHKEY_ID_RSA — private SSH key (тип: File или Variable, содержимое id_rsa)
|
||||
# ANSIBLE_BECOME_PASS — sudo password
|
||||
# GITLAB_FLUX_TOKEN — GitLab PAT (scope: api) для Flux bootstrap и клонирования fleet repo
|
||||
# GITLAB_AGENT_TOKEN — токен GitLab Agent (получить в GitLab → Operate → Kubernetes clusters)
|
||||
# LOKI_MINIO_PASSWORD — пароль пользователя loki в MinIO (создаётся при запуске setup_logging)
|
||||
# GRAFANA_ADMIN_PASSWORD — пароль администратора Grafana (задаётся в Secret вручную)
|
||||
|
||||
stages:
|
||||
- validate
|
||||
- setup
|
||||
|
||||
# ── Shared configuration ───────────────────────────────────────────────────────
|
||||
default:
|
||||
image: python:3.11-slim
|
||||
before_script:
|
||||
- apt-get update -qq
|
||||
- apt-get install -y -qq --no-install-recommends openssh-client git
|
||||
- pip install --quiet ansible ansible-lint netaddr
|
||||
- >
|
||||
for i in 1 2 3; do
|
||||
ansible-galaxy collection install -r requirements.yml -p collections/ --force && exit 0;
|
||||
echo "ansible-galaxy collection install failed (attempt $i/3), retrying in 5s...";
|
||||
sleep 5;
|
||||
done;
|
||||
exit 1
|
||||
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||
- printf '%b\n' "$ANSIBLE_SSHKEY_ID_RSA" > ~/.ssh/id_rsa
|
||||
- chmod 600 ~/.ssh/id_rsa
|
||||
- eval $(ssh-agent -s)
|
||||
- ssh-add ~/.ssh/id_rsa
|
||||
interruptible: true
|
||||
|
||||
variables:
|
||||
ANSIBLE_FORCE_COLOR: "1"
|
||||
ANSIBLE_HOST_KEY_CHECKING: "False"
|
||||
ANSIBLE_ROLES_PATH: roles
|
||||
ANSIBLE_COLLECTIONS_PATH: collections
|
||||
ANSIBLE_FILTER_PLUGINS: filter_plugins
|
||||
PIP_NO_CACHE_DIR: "1"
|
||||
INVENTORY: inventory/prod
|
||||
|
||||
# ── Stage: validate ────────────────────────────────────────────────────────────
|
||||
lint:
|
||||
stage: validate
|
||||
script:
|
||||
- ansible-lint playbooks/ roles/
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
|
||||
syntax-check:
|
||||
stage: validate
|
||||
script:
|
||||
- >
|
||||
ansible-playbook
|
||||
--syntax-check
|
||||
-i $INVENTORY
|
||||
playbooks/setup_manager.yml
|
||||
- >
|
||||
ansible-playbook
|
||||
--syntax-check
|
||||
-i $INVENTORY
|
||||
playbooks/setup_control_plane.yml
|
||||
- >
|
||||
ansible-playbook
|
||||
--syntax-check
|
||||
-i $INVENTORY
|
||||
playbooks/setup_worker_plane.yml
|
||||
- >
|
||||
ansible-playbook
|
||||
--syntax-check
|
||||
-i $INVENTORY
|
||||
playbooks/setup_longhorn.yml
|
||||
- >
|
||||
ansible-playbook
|
||||
--syntax-check
|
||||
-i $INVENTORY
|
||||
playbooks/setup_traefik.yml
|
||||
- >
|
||||
ansible-playbook
|
||||
--syntax-check
|
||||
-i $INVENTORY
|
||||
playbooks/setup_flux.yml
|
||||
- >
|
||||
ansible-playbook
|
||||
--syntax-check
|
||||
-i $INVENTORY
|
||||
playbooks/setup_gitlab_agent.yml
|
||||
- >
|
||||
ansible-playbook
|
||||
--syntax-check
|
||||
-i $INVENTORY
|
||||
playbooks/setup_minio.yml
|
||||
- >
|
||||
ansible-playbook
|
||||
--syntax-check
|
||||
-i $INVENTORY
|
||||
playbooks/setup_logging.yml
|
||||
- >
|
||||
ansible-playbook
|
||||
--syntax-check
|
||||
-i $INVENTORY
|
||||
playbooks/setup_sealed_secrets.yml
|
||||
- >
|
||||
ansible-playbook
|
||||
--syntax-check
|
||||
-i $INVENTORY
|
||||
playbooks/setup_dev_access.yml
|
||||
- >
|
||||
ansible-playbook
|
||||
--syntax-check
|
||||
-i $INVENTORY
|
||||
playbooks/setup_gpu.yml
|
||||
- >
|
||||
ansible-playbook
|
||||
--syntax-check
|
||||
-i $INVENTORY
|
||||
playbooks/setup_gpu_model_storage.yml
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
|
||||
# ── Stage: setup ───────────────────────────────────────────────────────────────
|
||||
setup:manager_nodes:
|
||||
stage: setup
|
||||
script:
|
||||
- >
|
||||
ansible-playbook
|
||||
-i $INVENTORY
|
||||
playbooks/setup_manager.yml
|
||||
environment:
|
||||
name: production
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
resource_group: production
|
||||
|
||||
setup:control_plane:
|
||||
stage: setup
|
||||
script:
|
||||
- >
|
||||
ansible-playbook
|
||||
-i $INVENTORY
|
||||
playbooks/setup_control_plane.yml
|
||||
environment:
|
||||
name: production
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
resource_group: production
|
||||
|
||||
setup:workers:
|
||||
stage: setup
|
||||
script:
|
||||
- >
|
||||
ansible-playbook
|
||||
-i $INVENTORY
|
||||
playbooks/setup_worker_plane.yml
|
||||
environment:
|
||||
name: production
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
resource_group: production
|
||||
|
||||
setup:longhorn:
|
||||
stage: setup
|
||||
script:
|
||||
- >
|
||||
ansible-playbook
|
||||
-i $INVENTORY
|
||||
playbooks/setup_longhorn.yml
|
||||
environment:
|
||||
name: production
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
resource_group: production
|
||||
|
||||
setup:traefik:
|
||||
stage: setup
|
||||
script:
|
||||
- >
|
||||
ansible-playbook
|
||||
-i $INVENTORY
|
||||
playbooks/setup_traefik.yml
|
||||
environment:
|
||||
name: production
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
resource_group: production
|
||||
|
||||
setup:flux:
|
||||
stage: setup
|
||||
script:
|
||||
- >
|
||||
ansible-playbook
|
||||
-i $INVENTORY
|
||||
playbooks/setup_flux.yml
|
||||
environment:
|
||||
name: production
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
resource_group: production
|
||||
|
||||
setup:gitlab_agent:
|
||||
stage: setup
|
||||
script:
|
||||
- >
|
||||
ansible-playbook
|
||||
-i $INVENTORY
|
||||
playbooks/setup_gitlab_agent.yml
|
||||
environment:
|
||||
name: production
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
resource_group: production
|
||||
|
||||
setup:minio:
|
||||
stage: setup
|
||||
script:
|
||||
- >
|
||||
ansible-playbook
|
||||
-i $INVENTORY
|
||||
playbooks/setup_minio.yml
|
||||
environment:
|
||||
name: production
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
resource_group: production
|
||||
|
||||
setup:logging:
|
||||
stage: setup
|
||||
script:
|
||||
- >
|
||||
ansible-playbook
|
||||
-i $INVENTORY
|
||||
playbooks/setup_logging.yml
|
||||
environment:
|
||||
name: production
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
resource_group: production
|
||||
|
||||
setup:sealed_secrets:
|
||||
stage: setup
|
||||
script:
|
||||
- >
|
||||
ansible-playbook
|
||||
-i $INVENTORY
|
||||
playbooks/setup_sealed_secrets.yml
|
||||
environment:
|
||||
name: production
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
resource_group: production
|
||||
|
||||
setup:dev_access:
|
||||
stage: setup
|
||||
script:
|
||||
- >
|
||||
ansible-playbook
|
||||
-i $INVENTORY
|
||||
playbooks/setup_dev_access.yml
|
||||
environment:
|
||||
name: production
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
resource_group: production
|
||||
|
||||
setup:gpu_workers:
|
||||
stage: setup
|
||||
script:
|
||||
- >
|
||||
ansible-playbook
|
||||
-i $INVENTORY
|
||||
playbooks/setup_gpu.yml
|
||||
environment:
|
||||
name: production
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
resource_group: production
|
||||
|
||||
setup:gpu_model_storage:
|
||||
stage: setup
|
||||
script:
|
||||
- >
|
||||
ansible-playbook
|
||||
-i $INVENTORY
|
||||
playbooks/setup_gpu_model_storage.yml
|
||||
environment:
|
||||
name: production
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
resource_group: production
|
||||
625
README.md
625
README.md
|
|
@ -1,2 +1,625 @@
|
|||
# k8s
|
||||
# Kubernetes Infrastructure
|
||||
|
||||
> **Корпоративный стандарт.** Ansible-развёртывание кластера Kubernetes на Rocky Linux 9.
|
||||
> Локальный запуск через CLI и автоматизированный запуск через GitLab CI/CD.
|
||||
|
||||
---
|
||||
|
||||
## Содержание
|
||||
|
||||
- [Инфраструктура](#инфраструктура)
|
||||
- [Стек технологий](#стек-технологий)
|
||||
- [Структура репозитория](#структура-репозитория)
|
||||
- [Роли и компоненты](#роли-и-компоненты)
|
||||
- - **Стандарт работы с хранилищем** [`roles/longhorn/README.md`](roles/longhorn/README.md)
|
||||
- - **Стандарт публикации сервисов** [`roles/traefik/README.md`](roles/traefik/README.md)
|
||||
- - **Стандарт GitOps-деплоя приложений** [`roles/flux/README.md`](roles/flux/README.md)
|
||||
- - **Стандарт объектного хранилища S3** [`roles/minio/README.md`](roles/minio/README.md)
|
||||
- - **Стандарт централизованного сбора логов** [`roles/logging/README.md`](roles/logging/README.md)
|
||||
- - Sealed Secrets (kubeseal)
|
||||
- [CI/CD-пайплайн](#cicd-пайплайн)
|
||||
- [Порядок развёртывания](#порядок-развёртывания)
|
||||
- [Архитектурные решения](#архитектурные-решения)
|
||||
- [Локальная разработка](#локальная-разработка)
|
||||
- [Добавление нового компонента](#добавление-нового-компонента)
|
||||
|
||||
---
|
||||
|
||||
## Инфраструктура
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Operator workstation (manager_nodes) │
|
||||
│ k8s-manager-01 10.203.0.92 │
|
||||
│ kubectl · helm · k9s · kubectx/kubens │
|
||||
│ Kubernetes Dashboard token → ~/.kube/dashboard-token │
|
||||
└────────────────────────┬────────────────────────────────────────┘
|
||||
│ kubectl / helm (kubeconfig)
|
||||
┌────────────────────────▼────────────────────────────────────────┐
|
||||
│ Kubernetes cluster (k8s_cluster) │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Control plane k8s-master-01 10.203.0.97 │ │
|
||||
│ │ kube-apiserver · etcd · controller-manager · scheduler │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Worker k8s-worker-01 10.203.0.96 │ │
|
||||
│ │ kubelet · kube-proxy · Longhorn disk agent │ │
|
||||
│ │ Traefik DaemonSet │ │
|
||||
│ │ :10001 → kubernetes-dashboard (ClusterIP :443) │ │
|
||||
│ │ :10002 → longhorn-frontend (ClusterIP :80) [auth] │ │
|
||||
│ │ :10003 → grafana (ClusterIP :80) │ │
|
||||
│ │ :10005 → minio (ClusterIP :9000) │ │
|
||||
│ │ :10006 → minio-console (ClusterIP :9001) │ │
|
||||
│ │ :10103 → db/bgbilling-dev (TCP :3306) │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ CNI: Flannel v0.26.1 │ Storage: Longhorn 1.7.2 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Узлы кластера
|
||||
|
||||
| Hostname | IP | Группа инвентаря | Роль |
|
||||
|---|---|---|---|
|
||||
| k8s-manager-01 | 10.203.0.92 | `manager_nodes` | Рабочая станция оператора |
|
||||
| k8s-master-01 | 10.203.0.97 | `control_plane` | API server, etcd, scheduler |
|
||||
| k8s-worker-01 | 10.203.0.96 | `workers` | Вычислительный узел |
|
||||
| k8s-worker-02 | 10.203.0.212 | `workers` → `gpu_workers` | GPU-узел (NVIDIA), Ollama-стек |
|
||||
| k8s-worker-NN | TBD | `workers` | Дополнительные вычислительные узлы |
|
||||
|
||||
> `manager_nodes` — **не** Kubernetes-узел. Это рабочая станция оператора, с которой запускаются `kubectl` и `helm`.
|
||||
|
||||
### Сервисы и точки доступа
|
||||
|
||||
| Сервис | URL | Аутентификация |
|
||||
|---|---|---|
|
||||
| Kubernetes Dashboard | `http://10.203.0.96:10001` | токен из `~/.kube/dashboard-token` |
|
||||
| Longhorn UI | `http://10.203.0.96:10002` | BasicAuth (Secret `traefik-auth-longhorn` в ns `traefik`) |
|
||||
| Grafana | `http://10.203.0.96:10003` | логин Grafana (admin, пароль из Secret `grafana-admin-secret`) |
|
||||
| MinIO S3 API | `http://10.203.0.96:10005` | AWS Signature v4 (встроенная аутентификация MinIO) |
|
||||
| MinIO Console | `http://10.203.0.96:10006` | логин MinIO (rootUser из Secret `minio-root-credentials`) |
|
||||
| MariaDB BGBilling | `10.203.0.96:10103` | TCP-проброс (без HTTP); аутентификация средствами MariaDB |
|
||||
| Ollama Proxy (k8s_ai) | `http://10.203.0.96:10200` | своя аутентификация приложения |
|
||||
| Open WebUI (k8s_ai) | `http://10.203.0.96:10201` | своя аутентификация приложения |
|
||||
|
||||
---
|
||||
|
||||
## Стек технологий
|
||||
|
||||
| Компонент | Версия | Управляется |
|
||||
|---|---|---|
|
||||
| ОС | Rocky Linux 9 | Ansible |
|
||||
| Kubernetes | 1.33 (pkgs.k8s.io) | Ansible → `k8s_control_plane`, `k8s_worker` |
|
||||
| Container runtime | containerd (Docker CE repo) | Ansible |
|
||||
| CNI | Flannel v0.26.1 / Calico v3.27.0 | Ansible → `k8s_control_plane` |
|
||||
| Постоянное хранилище | Longhorn 1.7.2 | Ansible → `longhorn_prereqs`, `longhorn` |
|
||||
| Ingress / proxy | Traefik 32.1.0 | Ansible → `traefik` |
|
||||
| Kubernetes Dashboard | v2.7.0 | Ansible → `k8s_manager` |
|
||||
| Объектное хранилище | MinIO 5.4.0 (chart) | Ansible → `minio` |
|
||||
| Централизованные логи | Loki 7.0.0 + Grafana Alloy 1.8.2 + Grafana 10.5.15 | Ansible → `logging` |
|
||||
| Шифрование Secret'ов | Sealed Secrets 2.16.1 | Ansible → `sealed_secrets` |
|
||||
| GitOps | Flux CD (latest stable) | Ansible → `flux` |
|
||||
| GitLab Agent | agentk (latest stable) | Ansible → `gitlab_agent` |
|
||||
| Автоматизация | Ansible (посм. requirements.yml) | — |
|
||||
| CI/CD | GitLab CI/CD | `.gitlab-ci.yml` |
|
||||
|
||||
---
|
||||
|
||||
## Структура репозитория
|
||||
|
||||
```
|
||||
.gitlab-ci.yml — CI/CD-пайплайн (стадии validate → setup)
|
||||
ansible.cfg — кэширование фактов, pipelining, YAML-вывод
|
||||
requirements.yml — коллекции Ansible Galaxy
|
||||
|
||||
inventory/prod/
|
||||
hosts.yml — статический список хостов (3 группы)
|
||||
group_vars/
|
||||
all.yml — SSH-учётные данные из переменных окружения
|
||||
k8s_cluster.yml — общие переменные для control_plane + workers
|
||||
control_plane.yml — k8s_version, CIDR, CNI
|
||||
workers.yml — конфигурация дисков Longhorn
|
||||
manager_nodes.yml — версии инструментов, часовой пояс
|
||||
traefik.yml — traefik_port_map: таблица портов и backend-сервисов
|
||||
gpu_workers.yml — node_labels, override дисков Longhorn для GPU-узлов
|
||||
|
||||
playbooks/ — один плейбук = одна CI/CD-задача
|
||||
setup_manager.yml
|
||||
setup_control_plane.yml
|
||||
setup_worker_plane.yml
|
||||
setup_longhorn.yml
|
||||
setup_traefik.yml
|
||||
setup_flux.yml
|
||||
setup_gitlab_agent.yml
|
||||
setup_minio.yml
|
||||
setup_logging.yml
|
||||
setup_sealed_secrets.yml
|
||||
setup_gpu.yml
|
||||
|
||||
roles/
|
||||
k8s_manager/
|
||||
k8s_control_plane/
|
||||
k8s_worker/
|
||||
longhorn_prereqs/
|
||||
longhorn/ README.md ← стандарт работы с хранилищем
|
||||
traefik/ README.md ← стандарт публикации сервисов
|
||||
flux/ README.md ← стандарт GitOps-деплоя приложений
|
||||
gitlab_agent/
|
||||
minio/ README.md ← стандарт объектного хранилища S3
|
||||
logging/ README.md ← стандарт централизованного сбора логов
|
||||
sealed_secrets/
|
||||
gpu_prereqs/ — nvidia-container-toolkit + containerd runtime на GPU-узле
|
||||
gpu_device_plugin/ — node label + NVIDIA k8s-device-plugin с manager-ноды
|
||||
gpu_model_storage/ README.md ← стандарт локального RAID1-хранилища под модели
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Роли и компоненты
|
||||
|
||||
### `k8s_control_plane` · `k8s_worker` · `k8s_manager`
|
||||
|
||||
Базовые роли развёртывания кластера. Не имеют отдельного role-README — полное описание в разделе [Архитектурные решения](#архитектурные-решения).
|
||||
|
||||
| Роль | Что делает | Плейбук |
|
||||
|---|---|---|
|
||||
| `k8s_control_plane` | containerd → kubelet → kubeadm init → CNI → kubeconfig | `setup_control_plane.yml` |
|
||||
| `k8s_worker` | containerd → kubelet → kubeadm join | `setup_worker_plane.yml` |
|
||||
| `k8s_manager` | kubectl / helm / k9s, Kubernetes Dashboard | `setup_manager.yml` |
|
||||
|
||||
<details>
|
||||
<summary>Переменные k8s_control_plane</summary>
|
||||
|
||||
| Переменная | Значение по умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `k8s_version` | `1.33` | Версия Kubernetes |
|
||||
| `pod_network_cidr` | `10.244.0.0/16` | Сеть подов (Flannel) |
|
||||
| `service_cidr` | `10.96.0.0/12` | Сеть сервисов |
|
||||
| `cni_plugin` | `flannel` | `flannel` или `calico` |
|
||||
| `flannel_version` | `v0.26.1` | Версия манифеста Flannel |
|
||||
| `calico_version` | `v3.27.0` | Версия манифеста Calico |
|
||||
| `kubeconfig_fetch_to_managers` | `true` | Копировать kubeconfig на manager-узел |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Переменные k8s_worker</summary>
|
||||
|
||||
| Переменная | Значение по умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `k8s_version` | `1.33` | Должна совпадать с control plane |
|
||||
| `worker_join_source_host` | первый хост `control_plane` | Откуда читать join-команду |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Переменные k8s_manager</summary>
|
||||
|
||||
| Переменная | Значение по умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `dashboard_insecure` | `false` | HTTP-режим Dashboard (переопределяется в `true` в group_vars) |
|
||||
| `dashboard_manifest_url` | GitHub raw v2.7.0 | Без зависимости от CDN |
|
||||
| `k8s_manager_upgrade_packages` | `false` | Полное обновление ОС (opt-in) |
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### `gpu_prereqs` · `gpu_device_plugin`
|
||||
|
||||
Подключение GPU-узла к кластеру. Драйвер NVIDIA ставится вручную заранее — роль его не устанавливает, только проверяет наличие (`nvidia-smi`). Запускается **после** `setup_worker_plane.yml`, отдельным плейбуком `setup_gpu.yml`, только для узлов группы `gpu_workers` (подгруппа `workers`).
|
||||
|
||||
| Роль | Запускается на | Что делает |
|
||||
|---|---|---|
|
||||
| `gpu_prereqs` | `gpu_workers` | Проверка драйвера, установка `nvidia-container-toolkit`, `nvidia-ctk runtime configure` для containerd (default runtime) |
|
||||
| `gpu_device_plugin` | `manager_nodes` | `kubectl label node <host> gpu=nvidia` для каждого узла `gpu_workers`; `kubectl apply` манифеста NVIDIA k8s-device-plugin |
|
||||
|
||||
<details>
|
||||
<summary>Переменные gpu_prereqs / gpu_device_plugin</summary>
|
||||
|
||||
| Переменная | Значение по умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `nvidia_container_toolkit_version` | `""` (последняя) | Версия пакета `nvidia-container-toolkit` |
|
||||
| `node_labels` | `{gpu: nvidia}` (в `gpu_workers.yml`) | Лейблы, навешиваемые на узел после join |
|
||||
| `gpu_device_plugin_version` | `v0.17.0` | Тег релиза NVIDIA k8s-device-plugin |
|
||||
| `gpu_device_plugin_manifest_url` | GitHub raw для указанной версии | Статический манифест DaemonSet |
|
||||
|
||||
</details>
|
||||
|
||||
> Порядок обязателен: `setup_worker_plane.yml` перезаписывает `/etc/containerd/config.toml` с нуля, поэтому `gpu_prereqs` (донастройка containerd под nvidia runtime) должна выполняться **после**, отдельным прогоном.
|
||||
|
||||
---
|
||||
|
||||
### `gpu_model_storage`
|
||||
|
||||
Выделенный локальный RAID1-раздел под файлы моделей (Ollama) на GPU-узлах. `gpu_workers` исключены из Longhorn (`longhorn_disks: []`), а корневая ФС слишком маленькая (~30 ГБ) для хранения моделей — поэтому под них нарезается отдельный раздел из неразмеченного места на тех же зеркальных дисках, что и ОС. Запускается плейбуком `setup_gpu_model_storage.yml`, независимо от порядка `kubeadm join`.
|
||||
|
||||
<details>
|
||||
<summary>Переменные gpu_model_storage</summary>
|
||||
|
||||
| Переменная | Значение по умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `gpu_model_storage_devices` | `[]` | Диски-члены RAID1 (пусто = роль выключена); задаётся в `gpu_workers.yml` |
|
||||
| `gpu_model_storage_partition_size_gb` | `1000` | Размер новой партиции на каждом диске, ГБ |
|
||||
| `gpu_model_storage_raid_device` | `/dev/md1` | Имя нового mdadm-массива |
|
||||
| `gpu_model_storage_mountpoint` | `/mnt/ollama-models` | Точка монтирования |
|
||||
| `gpu_model_storage_fstype` | `xfs` | Файловая система |
|
||||
|
||||
</details>
|
||||
|
||||
> **Standard**: [roles/gpu_model_storage/README.md](roles/gpu_model_storage/README.md)
|
||||
|
||||
---
|
||||
|
||||
### `longhorn_prereqs` · `longhorn`
|
||||
|
||||
Двухэтапное развёртывание распределённого блочного хранилища.
|
||||
|
||||
| Роль | Запускается на | Что делает |
|
||||
|---|---|---|
|
||||
| `longhorn_prereqs` | `workers` | iSCSI / NFS пакеты, модули ядра, SELinux CIL-политика, форматирование и монтирование дисков XFS |
|
||||
| `longhorn` | `manager_nodes` | Аннотации узлов с disk-config JSON, `helm upgrade --install longhorn` |
|
||||
|
||||
После установки доступен StorageClass `longhorn` (default). Приложения используют его через `storageClassName: longhorn` в PVC.
|
||||
|
||||
<details>
|
||||
<summary>Переменные</summary>
|
||||
|
||||
| Переменная | Значение по умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `longhorn_chart_version` | `1.7.2` | Версия Helm-чарта |
|
||||
| `longhorn_namespace` | `longhorn-system` | Namespace |
|
||||
| `longhorn_default_replica_count` | `1` | Реплик на том; поднять до 2–3 при добавлении worker-нод |
|
||||
| `longhorn_minimal_available_storage_percentage` | `10` | Минимальный свободный % хранилища |
|
||||
| `longhorn_storage_over_provisioning_percentage` | `100` | Коэффициент over-provisioning |
|
||||
| `longhorn_disks` | `[{device: /dev/sdb, mountpoint: /mnt/longhorn-disk1}, ...]` | Диски для форматирования; переопределяется в hosts.yml |
|
||||
|
||||
</details>
|
||||
|
||||
> **Стандарт работы с хранилищем** (PVC, StatefulSet, реплики, бэкапы, диагностика):
|
||||
> [`roles/longhorn/README.md`](roles/longhorn/README.md)
|
||||
|
||||
---
|
||||
|
||||
### `traefik`
|
||||
|
||||
HTTP/TCP port-proxy. DaemonSet на worker-нодах. Каждый сервис получает выделенный TCP-порт из диапазона 10001–10999. Вся топология определяется через `traefik_port_map` в `inventory/prod/group_vars/traefik.yml`. Поддерживаются два режима маршрутизации: HTTP (`IngressRoute`, поле `protocol` не задаётся) и raw TCP (`IngressRouteTCP` с `HostSNI("*")`, `protocol: tcp` в port_map).
|
||||
|
||||
| Задача | Что делает |
|
||||
|---|---|
|
||||
| `firewall.yml` | Открывает 10000–10999/tcp; помещает `flannel.1`, `cni0` в зону `trusted` |
|
||||
| `helm.yml` | `helm upgrade --install traefik` как DaemonSet с EntryPoint-ами из port_map |
|
||||
| `middleware.yml` | Применяет `Middleware` CRD (BasicAuth) для записей с `basicauth.enabled: true` |
|
||||
| `routes.yml` | Применяет `IngressRoute` (HTTP) или `IngressRouteTCP` (TCP) в зависимости от `protocol` в port_map |
|
||||
|
||||
<details>
|
||||
<summary>Переменные</summary>
|
||||
|
||||
| Переменная | Значение по умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `traefik_chart_version` | `32.1.0` | Версия Helm-чарта |
|
||||
| `traefik_namespace` | `traefik` | Namespace |
|
||||
| `traefik_port_map` | `[]` | Список сервисов; переопределяется в `group_vars/traefik.yml` |
|
||||
|
||||
</details>
|
||||
|
||||
> **Стандарт публикации сервисов** (добавление порта, BasicAuth, диагностика, таблица активных портов):
|
||||
> [`roles/traefik/README.md`](roles/traefik/README.md)
|
||||
|
||||
---
|
||||
|
||||
### `flux`
|
||||
|
||||
Устанавливает Flux CD CLI и выполняет `flux bootstrap gitlab`, подключая кластер к fleet-репозиторию `k8s-fleet` на `gitlab.gigacoms.info`.
|
||||
|
||||
| Задача | Что делает |
|
||||
|---|---|
|
||||
| `install.yml` | Flux CLI → `/usr/local/bin/flux`, bash-completion, `flux check --pre` |
|
||||
| `bootstrap.yml` | `flux bootstrap gitlab` — deploy key, Flux-контроллеры в `flux-system`, манифесты в `clusters/production` |
|
||||
|
||||
Fleet-репозиторий: `gitlab.gigacoms.info/k8s/k8s-fleet` · путь: `clusters/production`
|
||||
|
||||
<details>
|
||||
<summary>Переменные</summary>
|
||||
|
||||
| Переменная | Значение по умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `flux_version` | `""` (latest stable) | Версия Flux CLI |
|
||||
| `flux_gitlab_hostname` | `gitlab.gigacoms.info` | Хост GitLab |
|
||||
| `flux_gitlab_owner` | `k8s` | GitLab-группа fleet-репозитория |
|
||||
| `flux_gitlab_repository` | `k8s-fleet` | Имя fleet-репозитория |
|
||||
| `flux_gitlab_branch` | `main` | Ветка |
|
||||
| `flux_gitlab_path` | `clusters/production` | Путь внутри репозитория |
|
||||
| `flux_gitlab_token` | из `$GITLAB_FLUX_TOKEN` | GitLab PAT (scope: api) |
|
||||
|
||||
</details>
|
||||
|
||||
> **Стандарт GitOps-деплоя приложений** (структура fleet-repo, multi-branch, примеры с Longhorn и Traefik, CI/CD-шаблон):
|
||||
> [`roles/flux/README.md`](roles/flux/README.md)
|
||||
|
||||
---
|
||||
|
||||
### `minio`
|
||||
|
||||
S3-совместимое объектное хранилище на базе MinIO. Запускается как standalone StatefulSet на worker-ноде, хранилище предоставляется отдельным StorageClass `longhorn-minio` (reclaimPolicy: Retain). Доступ — через Traefik на портах 10005 (S3 API) и 10006 (Console UI).
|
||||
|
||||
| Задача | Что делает |
|
||||
|---|---|
|
||||
| `storageclass.yml` | Создаёт StorageClass `longhorn-minio` (Retain, numberOfReplicas=1) |
|
||||
| `namespace.yml` | Создаёт namespace `minio` |
|
||||
| `helm.yml` | `helm upgrade --install minio minio/minio` с values из шаблона |
|
||||
|
||||
**Предварительное условие:** создать Secret `minio-root-credentials` вручную в namespace `minio`:
|
||||
```bash
|
||||
kubectl create secret generic minio-root-credentials \
|
||||
--from-literal=rootUser=minioadmin \
|
||||
--from-literal=rootPassword='СИЛЬНЫЙ_ПАРОЛЬ' \
|
||||
-n minio
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Переменные</summary>
|
||||
|
||||
| Переменная | Значение по умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `minio_chart_version` | `5.4.0` | Версия Helm-чарта `minio/minio` |
|
||||
| `minio_image_tag` | `RELEASE.2025-04-22T22-12-26Z` | Тег образа MinIO |
|
||||
| `minio_namespace` | `minio` | Namespace |
|
||||
| `minio_storage_class` | `longhorn-minio` | StorageClass для PVC |
|
||||
| `minio_storage_size` | `1Ti` | Размер PVC |
|
||||
| `minio_console_url` | `http://10.203.0.96:10006` | URL Console (для MINIO_BROWSER_REDIRECT_URL) |
|
||||
| `minio_root_secret` | `minio-root-credentials` | Имя Secret с rootUser/rootPassword |
|
||||
| `minio_buckets` | `[backups, artifacts]` | Бакеты, создаваемые при старте |
|
||||
|
||||
</details>
|
||||
|
||||
> **Стандарт объектного хранилища S3** (StorageClass, пользователи, lifecycle, интеграция с Loki, масштабирование):
|
||||
> [`roles/minio/README.md`](roles/minio/README.md)
|
||||
|
||||
---
|
||||
|
||||
### `logging`
|
||||
|
||||
PLG-стек централизованного сбора логов: **Loki** (S3 backend → MinIO), **Grafana Alloy** (DaemonSet, замена EOL Promtail), **Grafana** (UI + алерты). Все три компонента разворачиваются в namespace `monitoring` из manager-узла через Helm.
|
||||
|
||||
| Задача | Что делает |
|
||||
|---|---|
|
||||
| `namespace.yml` | Создаёт namespace `monitoring` |
|
||||
| `minio-user.yml` | Создаёт bucket `loki-chunks`, пользователя `loki` с политикой только на этот bucket, Secret `loki-minio-secret` |
|
||||
| `loki.yml` | `helm upgrade --install loki` в режиме single-binary с S3 backend (MinIO) |
|
||||
| `alloy.yml` | `helm upgrade --install alloy` как DaemonSet — сбор логов подов + journald на всех нодах |
|
||||
| `grafana.yml` | `helm upgrade --install grafana` с provisioning datasource Loki |
|
||||
|
||||
**Предварительное условие:** создать Secret `grafana-admin-secret` вручную в namespace `monitoring`:
|
||||
```bash
|
||||
kubectl create namespace monitoring
|
||||
kubectl create secret generic grafana-admin-secret \
|
||||
--from-literal=admin-user=admin \
|
||||
--from-literal=admin-password='СИЛЬНЫЙ_ПАРОЛЬ' \
|
||||
-n monitoring
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Переменные</summary>
|
||||
|
||||
| Переменная | Значение по умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `loki_chart_version` | `7.0.0` | Версия Helm-чарта `grafana/loki` |
|
||||
| `alloy_chart_version` | `1.8.2` | Версия Helm-чарта `grafana/alloy` |
|
||||
| `grafana_chart_version` | `10.5.15` | Версия Helm-чарта `grafana/grafana` |
|
||||
| `loki_minio_endpoint` | `http://minio.minio.svc.cluster.local:9000` | S3 endpoint для Loki |
|
||||
| `loki_minio_bucket` | `loki-chunks` | Bucket для хранения чанков и индексов |
|
||||
| `loki_retention_days` | `31` | Срок хранения логов в днях |
|
||||
| `loki_wal_storage_size` | `10Gi` | PVC для WAL/temp Loki (не логи — они в MinIO) |
|
||||
| `grafana_storage_size` | `5Gi` | PVC для базы данных Grafana |
|
||||
| `grafana_root_url` | `http://10.203.0.96:10003` | Внешний URL Grafana (Traefik) |
|
||||
| `grafana_admin_secret` | `grafana-admin-secret` | Имя Secret с admin-user/admin-password |
|
||||
|
||||
</details>
|
||||
|
||||
> **Стандарт централизованного сбора логов** (архитектура PLG, масштабирование, переход на Graylog, диагностика):
|
||||
> [`roles/logging/README.md`](roles/logging/README.md)
|
||||
|
||||
---
|
||||
|
||||
### `sealed_secrets`
|
||||
|
||||
Разворачивает [Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) — контроллер для безопасного хранения зашифрованных Secret'ов в Git. Запускается из manager-узла: контроллер — в `kube-system` через Helm, CLI `kubeseal` — на manager-узле.
|
||||
|
||||
| Задача | Что делает |
|
||||
|---|---|
|
||||
| `cli.yml` | Скачивает `kubeseal` с GitHub Releases (версия из переменной или latest), идемпотентно |
|
||||
| `helm.yml` | `helm upgrade --install sealed-secrets` из `bitnami-labs/sealed-secrets` в `kube-system` |
|
||||
|
||||
**Использование после установки:**
|
||||
```bash
|
||||
# Зашифровать Secret
|
||||
kubectl create secret generic my-secret --from-literal=password=mysecret --dry-run=client -o yaml \
|
||||
| kubeseal --format yaml > my-sealedsecret.yaml
|
||||
|
||||
# Применить (можно коммитить в Git — зашифрован публичным ключом контроллера)
|
||||
kubectl apply -f my-sealedsecret.yaml
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Переменные</summary>
|
||||
|
||||
| Переменная | Значение по умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `sealed_secrets_chart_version` | `2.16.1` | Версия Helm-чарта |
|
||||
| `sealed_secrets_namespace` | `kube-system` | Namespace контроллера |
|
||||
| `sealed_secrets_cli_version` | `""` (latest) | Версия kubeseal CLI; пустая — берёт latest с GitHub |
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### `gitlab_agent`
|
||||
|
||||
Устанавливает GitLab Agent for Kubernetes (agentk). Работает совместно с Flux: Flux деплоит, agentk даёт видимость в GitLab UI (Operate → Kubernetes clusters).
|
||||
|
||||
Agentk инициирует исходящее WebSocket-соединение к KAS — входящих портов не требует.
|
||||
|
||||
| Задача | Что делает |
|
||||
|---|---|
|
||||
| `config.yml` | Клонирует fleet-repo, создаёт `.gitlab/agents/production/config.yaml`, коммитит и пушит при изменении |
|
||||
| `helm.yml` | `helm upgrade --install gitlab-agent` с токеном и KAS-адресом |
|
||||
|
||||
**Предварительное условие:** зарегистрировать агент в GitLab UI → Operate → Kubernetes clusters → Connect a cluster → имя `production` → токен в CI/CD переменную `GITLAB_AGENT_TOKEN`.
|
||||
|
||||
<details>
|
||||
<summary>Переменные</summary>
|
||||
|
||||
| Переменная | Значение по умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `gitlab_agent_hostname` | `gitlab.gigacoms.info` | Хост GitLab |
|
||||
| `gitlab_agent_name` | `production` | Имя агента (совпадает с именем в GitLab UI) |
|
||||
| `gitlab_agent_namespace` | `gitlab-agent` | Namespace |
|
||||
| `gitlab_agent_kas_address` | `wss://gitlab.gigacoms.info/-/kubernetes-agent/` | KAS WebSocket-адрес |
|
||||
| `gitlab_agent_token` | из `$GITLAB_AGENT_TOKEN` | Токен агента |
|
||||
| `gitlab_agent_ci_access_group` | `k8s` | GitLab-группа с доступом к кластеру через CI |
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## CI/CD-пайплайн
|
||||
|
||||
Стадии: **validate** → **setup**
|
||||
|
||||
| Задача | Триггер | Описание |
|
||||
|---|---|---|
|
||||
| `validate/lint` | каждый push / MR | `ansible-lint` всех плейбуков и ролей |
|
||||
| `validate/syntax-check` | каждый push / MR | Синтаксическая проверка всех плейбуков |
|
||||
| `setup/manager_nodes` | ручной, ветка `main` | `setup_manager.yml` |
|
||||
| `setup/control_plane` | ручной, ветка `main` | `setup_control_plane.yml` |
|
||||
| `setup/workers` | ручной, ветка `main` | `setup_worker_plane.yml` |
|
||||
| `setup/longhorn` | ручной, ветка `main` | `setup_longhorn.yml` |
|
||||
| `setup/traefik` | ручной, ветка `main` | `setup_traefik.yml` |
|
||||
| `setup/flux` | ручной, ветка `main` | `setup_flux.yml` |
|
||||
| `setup/gitlab_agent` | ручной, ветка `main` | `setup_gitlab_agent.yml` |
|
||||
| `setup/minio` | ручной, ветка `main` | `setup_minio.yml` |
|
||||
| `setup/logging` | ручной, ветка `main` | `setup_logging.yml` |
|
||||
| `setup/sealed_secrets` | ручной, ветка `main` | `setup_sealed_secrets.yml` |
|
||||
| `setup/gpu_workers` | ручной, ветка `main` | `setup_gpu.yml` |
|
||||
| `setup/gpu_model_storage` | ручной, ветка `main` | `setup_gpu_model_storage.yml` |
|
||||
|
||||
`resource_group: production` — все setup-задачи сериализованы, одновременно выполняется только одна.
|
||||
|
||||
### Переменные GitLab CI/CD (Settings → CI/CD → Variables)
|
||||
|
||||
| Переменная | Описание |
|
||||
|---|---|
|
||||
| `ANSIBLE_SSH_USER` | SSH-пользователь для целевых серверов |
|
||||
| `ANSIBLE_SSHKEY_ID_RSA` | Содержимое приватного SSH-ключа (тип: File) |
|
||||
| `ANSIBLE_BECOME_PASS` | Пароль sudo |
|
||||
| `GITLAB_FLUX_TOKEN` | GitLab PAT (scope: api) для Flux bootstrap и клонирования fleet-repo |
|
||||
| `GITLAB_AGENT_TOKEN` | Токен GitLab Agent (получить в GitLab → Operate → Kubernetes clusters) |
|
||||
| `MINIO_ROOT_PASSWORD` | Пароль root-пользователя MinIO (используется при ручном создании Secret) |
|
||||
| `LOKI_MINIO_PASSWORD` | Пароль пользователя `loki` в MinIO (создаётся Ansible при запуске setup_logging) |
|
||||
| `GRAFANA_ADMIN_PASSWORD` | Пароль администратора Grafana (задаётся в Secret `grafana-admin-secret` вручную) |
|
||||
|
||||
---
|
||||
|
||||
## Порядок развёртывания
|
||||
|
||||
Для нового кластера — строго в следующем порядке:
|
||||
|
||||
```
|
||||
1. setup:manager_nodes — инструменты оператора
|
||||
2. setup:control_plane — Kubernetes, CNI, Dashboard
|
||||
3. setup:workers — подключение worker-нод
|
||||
4. setup:longhorn — хранилище (prereqs на workers → Helm с manager)
|
||||
5. setup:traefik — порт-прокси (firewall на workers → Helm с manager)
|
||||
6. setup:minio — объектное хранилище S3 (StorageClass + Helm с manager)
|
||||
7. setup:flux — GitOps bootstrap
|
||||
8. setup:gitlab_agent — видимость кластера в GitLab UI
|
||||
9. setup:logging — PLG-стек (Loki + Alloy + Grafana)
|
||||
10. setup:sealed_secrets — контроллер Sealed Secrets + kubeseal CLI
|
||||
11. setup:gpu_workers — только для узлов gpu_workers, после их setup:workers
|
||||
```
|
||||
|
||||
> **Перед запуском setup:minio** создать Secret вручную: `kubectl create secret generic minio-root-credentials --from-literal=rootUser=minioadmin --from-literal=rootPassword='...' -n minio`
|
||||
|
||||
> **Перед запуском setup:logging** создать Secret вручную: `kubectl create namespace monitoring && kubectl create secret generic grafana-admin-secret --from-literal=admin-user=admin --from-literal=admin-password='...' -n monitoring`
|
||||
|
||||
---
|
||||
|
||||
## Архитектурные решения
|
||||
|
||||
### kubeadm: version derive at runtime
|
||||
`kubernetesVersion` в конфиге kubeadm определяется из `kubeadm version -o short` в runtime. Исключает ошибки несоответствия версий при изменении пакета без изменения переменной.
|
||||
|
||||
### single-node: снятие control-plane taint
|
||||
Taint `node-role.kubernetes.io/control-plane:NoSchedule` снимается автоматически. Dashboard и прочие workload'ы размещаются на master-узле при single-node-конфигурации.
|
||||
|
||||
### kubeadm join: ежезапускная генерация токена
|
||||
Join-токены истекают через 24 часа. Роль `k8s_worker` генерирует свежий токен на control plane при каждом запуске; шаг join идемпотентен — пропускается, если узел уже в кластере.
|
||||
|
||||
### Longhorn: монтирование до аннотации
|
||||
Диски форматируются как XFS и монтируются по UUID (`nofail`) **до** установки Longhorn. Longhorn обнаруживает диски через аннотации узлов — не через автоопределение. Это предотвращает переформатирование при повторном запуске плейбука.
|
||||
|
||||
### Traefik: нет hostname, нет TLS
|
||||
Каждый сервис получает выделенный порт (10001–10999). Нет hostname-routing, нет TLS — клиент подключается по `http://<worker-ip>:<port>`. TLS терминируется на уровне Traefik при необходимости. Dashboard работает в HTTP-режиме (`dashboard_insecure: true`).
|
||||
|
||||
### Flux + agentk: разделение ответственности
|
||||
Ansible управляет **ОС и кластером** (установка, конфигурация нод). Flux управляет **приложениями внутри кластера** (деплой, обновление образов). agentk обеспечивает **видимость** состояния кластера в GitLab UI.
|
||||
|
||||
### GPU-узлы: отдельный плейбук, вручную ставится только драйвер
|
||||
`gpu_workers` — подгруппа `workers`. NVIDIA-драйвер ставится на сервер вручную и не управляется Ansible (роль `gpu_prereqs` лишь проверяет `nvidia-smi` и падает, если драйвера нет). `setup_gpu.yml` запускается отдельно и **после** `setup_worker_plane.yml`, так как роль `k8s_worker` перегенерирует `/etc/containerd/config.toml` с нуля и стёрла бы настройку nvidia runtime, если бы порядок был обратным. GPU-узлы исключены из дефолтной конфигурации Longhorn (`longhorn_disks: []` в `gpu_workers.yml`) — это compute-нода, не storage-нода.
|
||||
|
||||
### Firewalld и CNI-интерфейсы
|
||||
|
||||
> **Критично.** `flannel.1` и `cni0` должны быть в зоне `trusted` на каждом узле кластера.
|
||||
|
||||
Без этого pod-to-pod-трафик от workers, приходящий на master, обрабатывается зоной `internal` — она пропускает только явно перечисленные порты. Любой порт нового сервиса молча отбрасывается, даже если порт открыт на уровне хоста.
|
||||
|
||||
При добавлении любой роли с `firewalld` — обязательно добавлять:
|
||||
|
||||
```yaml
|
||||
- name: Firewall | Trust CNI interfaces
|
||||
ansible.posix.firewalld:
|
||||
zone: trusted
|
||||
interface: "{{ item }}"
|
||||
permanent: true
|
||||
state: enabled
|
||||
loop:
|
||||
- flannel.1
|
||||
- cni0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Локальная разработка
|
||||
|
||||
```bash
|
||||
# Установка коллекций Ansible Galaxy
|
||||
ansible-galaxy collection install -r requirements.yml -p collections/ --force
|
||||
|
||||
# Проверка синтаксиса
|
||||
ansible-playbook --syntax-check -i inventory/prod playbooks/setup_manager.yml
|
||||
|
||||
# Линтинг
|
||||
ansible-lint playbooks/ roles/
|
||||
|
||||
# Dry-run (режим check + diff)
|
||||
ansible-playbook -i inventory/prod playbooks/setup_control_plane.yml --check --diff
|
||||
|
||||
# Запуск только на одном хосте
|
||||
ansible-playbook -i inventory/prod playbooks/setup_worker_plane.yml --limit k8s-worker-01
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Добавление нового компонента
|
||||
|
||||
1. Добавить хост в `inventory/prod/hosts.yml` (если новая нода)
|
||||
2. Создать `inventory/prod/group_vars/<group>.yml` (если новая группа)
|
||||
3. Создать роль `roles/<role>/` со стандартной структурой
|
||||
4. Создать `roles/<role>/README.md` — подробный корп. стандарт использования компонента
|
||||
5. Создать `playbooks/<operation>.yml`
|
||||
6. Добавить CI/CD-задачу в `.gitlab-ci.yml` (`stage: setup`, `resource_group: production`)
|
||||
7. Обновить **этот README**: краткое описание роли + ссылка на `roles/<role>/README.md`
|
||||
8. Обновить `CLAUDE.md`: добавить роль в структуру репозитория и ключевые решения
|
||||
|
|
|
|||
17
ansible.cfg
Normal file
17
ansible.cfg
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[defaults]
|
||||
inventory = inventory/prod
|
||||
roles_path = roles
|
||||
collections_path = collections
|
||||
filter_plugins = filter_plugins
|
||||
host_key_checking = False
|
||||
stdout_callback = ansible.builtin.default
|
||||
result_format = yaml
|
||||
gathering = smart
|
||||
fact_caching = jsonfile
|
||||
fact_caching_connection = /tmp/ansible_facts_cache
|
||||
fact_caching_timeout = 86400
|
||||
retry_files_enabled = False
|
||||
|
||||
[ssh_connection]
|
||||
pipelining = True
|
||||
ssh_args = -o ControlMaster=auto -o ControlPersist=300s
|
||||
5
inventory/prod/group_vars/all.yml
Normal file
5
inventory/prod/group_vars/all.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
ansible_user: "{{ lookup('env', 'ANSIBLE_SSH_USER') | default('ansible') }}"
|
||||
ansible_become: true
|
||||
ansible_become_method: sudo
|
||||
ansible_become_pass: "{{ lookup('env', 'ANSIBLE_BECOME_PASS') | default(omit) }}"
|
||||
4
inventory/prod/group_vars/control_plane.yml
Normal file
4
inventory/prod/group_vars/control_plane.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
k8s_version: "1.33"
|
||||
cni_plugin: "flannel"
|
||||
kubeconfig_fetch_to_managers: true
|
||||
26
inventory/prod/group_vars/gpu_workers.yml
Normal file
26
inventory/prod/group_vars/gpu_workers.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
# Settings specific to workers with an attached NVIDIA GPU.
|
||||
# This group is a child of `workers` — it inherits workers.yml and only
|
||||
# overrides/adds what differs for a GPU node.
|
||||
|
||||
# GPU nodes are not Longhorn storage nodes — override the workers.yml default
|
||||
# (which assumes /dev/sdb + /dev/sdc are present) so setup_longhorn.yml does
|
||||
# not try to format disks that don't exist in that layout.
|
||||
longhorn_disks: []
|
||||
|
||||
# Applied to the node object in Kubernetes by roles/gpu_device_plugin after
|
||||
# kubeadm join, so ollama/other GPU workloads can target it via nodeSelector.
|
||||
node_labels:
|
||||
gpu: nvidia
|
||||
|
||||
# nvidia-container-toolkit package version (empty = latest)
|
||||
nvidia_container_toolkit_version: ""
|
||||
|
||||
# Dedicated local RAID1 storage for Ollama model files (see roles/gpu_model_storage).
|
||||
# Carved out of otherwise-unpartitioned space on the same mirrored disks used
|
||||
# for the OS (sda/sdb have ~3.6TB free beyond the ~30GB OS RAID1 partition) —
|
||||
# model data must NOT go on the root filesystem, it's far too small.
|
||||
gpu_model_storage_devices:
|
||||
- /dev/sda
|
||||
- /dev/sdb
|
||||
gpu_model_storage_partition_size_gb: 1000
|
||||
7
inventory/prod/group_vars/k8s_cluster.yml
Normal file
7
inventory/prod/group_vars/k8s_cluster.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
# Variables shared across all cluster nodes (control_plane + workers)
|
||||
|
||||
# Pod and service CIDRs — must match kubeadm config in control_plane role.
|
||||
# Defined here so both control_plane and worker firewall rules can reference them.
|
||||
pod_network_cidr: "10.244.0.0/16"
|
||||
service_cidr: "10.96.0.0/12"
|
||||
32
inventory/prod/group_vars/manager_nodes.yml
Normal file
32
inventory/prod/group_vars/manager_nodes.yml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
# Kubernetes management node settings
|
||||
|
||||
# GitLab Agent: additional projects allowed to use the agent via CI/CD
|
||||
gitlab_agent_ci_access_projects:
|
||||
- it-dept/billing-mobile
|
||||
dashboard_insecure: true
|
||||
|
||||
k8s_manager_timezone: "Europe/Moscow"
|
||||
|
||||
# kubectl version (empty = latest stable)
|
||||
kubectl_version: ""
|
||||
|
||||
# Helm version (empty = latest stable)
|
||||
helm_version: ""
|
||||
|
||||
# k9s version (empty = latest stable)
|
||||
k9s_version: ""
|
||||
|
||||
# Additional tools to install
|
||||
k8s_manager_extra_packages:
|
||||
- bash-completion
|
||||
- curl
|
||||
- wget
|
||||
- git
|
||||
- vim
|
||||
- htop
|
||||
- net-tools
|
||||
- bind-utils
|
||||
- jq
|
||||
- python3
|
||||
- python3-pip
|
||||
146
inventory/prod/group_vars/traefik.yml
Normal file
146
inventory/prod/group_vars/traefik.yml
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
---
|
||||
|
||||
traefik_port_map:
|
||||
|
||||
- name: k8s-dashboard
|
||||
description: "Kubernetes Dashboard"
|
||||
port: 10001
|
||||
backend:
|
||||
namespace: kubernetes-dashboard
|
||||
service: kubernetes-dashboard
|
||||
port: 443
|
||||
scheme: http
|
||||
basicauth:
|
||||
enabled: false
|
||||
|
||||
- name: grafana
|
||||
description: "Grafana (Logs & Metrics)"
|
||||
port: 10003
|
||||
backend:
|
||||
namespace: monitoring
|
||||
service: grafana
|
||||
port: 80
|
||||
scheme: http
|
||||
basicauth:
|
||||
enabled: false
|
||||
|
||||
- name: longhorn-ui
|
||||
description: "Longhorn Storage UI"
|
||||
port: 10002
|
||||
backend:
|
||||
namespace: longhorn-system
|
||||
service: longhorn-frontend
|
||||
port: 80
|
||||
scheme: http
|
||||
basicauth:
|
||||
enabled: true
|
||||
secret_name: traefik-auth-longhorn
|
||||
|
||||
- name: minio-api
|
||||
description: "MinIO S3 API"
|
||||
port: 10005
|
||||
backend:
|
||||
namespace: minio
|
||||
service: minio
|
||||
port: 9000
|
||||
scheme: http
|
||||
basicauth:
|
||||
enabled: false
|
||||
|
||||
- name: minio-console
|
||||
description: "MinIO Console (Web UI)"
|
||||
port: 10006
|
||||
backend:
|
||||
namespace: minio
|
||||
service: minio-console
|
||||
port: 9001
|
||||
scheme: http
|
||||
basicauth:
|
||||
enabled: false
|
||||
|
||||
- name: billing-backend
|
||||
description: "Mobile Billing Backend API"
|
||||
port: 10100
|
||||
backend:
|
||||
namespace: gigacom-billing-mobile
|
||||
service: billing-backend
|
||||
port: 8080
|
||||
scheme: http
|
||||
basicauth:
|
||||
enabled: false
|
||||
|
||||
- name: bgbilling
|
||||
description: "BGBillingServer HTTP (web UI, API)"
|
||||
port: 10101
|
||||
backend:
|
||||
namespace: bgbilling-dev
|
||||
service: bgbilling
|
||||
port: 8080
|
||||
scheme: http
|
||||
basicauth:
|
||||
enabled: false
|
||||
|
||||
- name: bgbilling-https
|
||||
description: "BGBillingServer HTTPS (Tomcat TLS, .keystore)"
|
||||
port: 10102
|
||||
backend:
|
||||
namespace: bgbilling-dev
|
||||
service: bgbilling
|
||||
port: 8443
|
||||
scheme: https
|
||||
basicauth:
|
||||
enabled: false
|
||||
|
||||
- name: bgbilling-db
|
||||
description: "MariaDB for BGBilling (TCP)"
|
||||
protocol: tcp
|
||||
port: 10103
|
||||
backend:
|
||||
namespace: bgbilling-dev
|
||||
service: db
|
||||
port: 3306
|
||||
|
||||
- name: activemq-web
|
||||
description: "ActiveMQ Web Console"
|
||||
port: 10104
|
||||
backend:
|
||||
namespace: bgbilling-dev
|
||||
service: activemq
|
||||
port: 8161
|
||||
scheme: http
|
||||
basicauth:
|
||||
enabled: false
|
||||
|
||||
- name: ollama-proxy
|
||||
description: "Ollama Proxy API (k8s_ai)"
|
||||
port: 10200
|
||||
backend:
|
||||
namespace: ai
|
||||
service: ollama-proxy
|
||||
port: 8080
|
||||
scheme: http
|
||||
basicauth:
|
||||
enabled: false
|
||||
|
||||
- name: open-webui
|
||||
description: "Open WebUI (k8s_ai)"
|
||||
port: 10201
|
||||
backend:
|
||||
namespace: ai
|
||||
service: open-webui
|
||||
port: 8080
|
||||
scheme: http
|
||||
basicauth:
|
||||
enabled: false
|
||||
|
||||
# Template for future services:
|
||||
# - name: my-service
|
||||
# description: "Human-readable description"
|
||||
# port: 10007
|
||||
# backend:
|
||||
# namespace: my-namespace
|
||||
# service: my-service-name
|
||||
# port: 8080
|
||||
# scheme: http
|
||||
# basicauth:
|
||||
# enabled: false
|
||||
12
inventory/prod/group_vars/workers.yml
Normal file
12
inventory/prod/group_vars/workers.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
# Kubernetes worker (compute) node settings
|
||||
|
||||
# node_labels: {}
|
||||
# node_taints: []
|
||||
|
||||
# Disks dedicated to Longhorn storage (override per host in hosts.yml if needed)
|
||||
longhorn_disks:
|
||||
- device: /dev/sdb
|
||||
mountpoint: /mnt/longhorn-disk1
|
||||
- device: /dev/sdc
|
||||
mountpoint: /mnt/longhorn-disk2
|
||||
26
inventory/prod/hosts.yml
Normal file
26
inventory/prod/hosts.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
all:
|
||||
children:
|
||||
# Operator workstation: kubectl, helm, k9s
|
||||
manager_nodes:
|
||||
hosts:
|
||||
k8s-manager-01:
|
||||
ansible_host: 10.203.0.92
|
||||
|
||||
# Kubernetes cluster nodes
|
||||
k8s_cluster:
|
||||
children:
|
||||
control_plane:
|
||||
hosts:
|
||||
k8s-master-01:
|
||||
ansible_host: 10.203.0.97
|
||||
workers:
|
||||
hosts:
|
||||
k8s-worker-01:
|
||||
ansible_host: 10.203.0.96
|
||||
children:
|
||||
# Workers with an NVIDIA GPU attached (device plugin + node label)
|
||||
gpu_workers:
|
||||
hosts:
|
||||
k8s-worker-02:
|
||||
ansible_host: 10.203.0.212
|
||||
6
playbooks/setup_control_plane.yml
Normal file
6
playbooks/setup_control_plane.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: Setup Kubernetes control plane
|
||||
hosts: control_plane
|
||||
gather_facts: true
|
||||
roles:
|
||||
- k8s_control_plane
|
||||
5
playbooks/setup_flux.yml
Normal file
5
playbooks/setup_flux.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
- name: Flux | Install CLI and bootstrap with GitLab
|
||||
hosts: manager_nodes
|
||||
roles:
|
||||
- flux
|
||||
6
playbooks/setup_gbm_dev_access.yml
Normal file
6
playbooks/setup_gbm_dev_access.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: Setup developer access token for namespace
|
||||
hosts: manager_nodes
|
||||
gather_facts: false
|
||||
roles:
|
||||
- role: k8s_gbm_dev
|
||||
5
playbooks/setup_gitlab_agent.yml
Normal file
5
playbooks/setup_gitlab_agent.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
- name: GitLab Agent | Push agent config to fleet repo and install agentk
|
||||
hosts: manager_nodes
|
||||
roles:
|
||||
- gitlab_agent
|
||||
13
playbooks/setup_gpu.yml
Normal file
13
playbooks/setup_gpu.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
# Run AFTER setup_worker_plane.yml (needs kubeadm join + containerd already done)
|
||||
# and after the target node has joined the cluster.
|
||||
- name: GPU | Configure container runtime on GPU worker nodes
|
||||
hosts: gpu_workers
|
||||
become: true
|
||||
roles:
|
||||
- gpu_prereqs
|
||||
|
||||
- name: GPU | Label nodes and install NVIDIA device plugin
|
||||
hosts: manager_nodes
|
||||
roles:
|
||||
- gpu_device_plugin
|
||||
10
playbooks/setup_gpu_model_storage.yml
Normal file
10
playbooks/setup_gpu_model_storage.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
# Provisions a dedicated local RAID1 partition for large model files (Ollama)
|
||||
# on GPU worker nodes, carved out of unpartitioned space on the same mirrored
|
||||
# OS disks. Run any time after the node's disks are in place; independent of
|
||||
# kubeadm join order.
|
||||
- name: GPU | Provision dedicated RAID1 storage for model files
|
||||
hosts: gpu_workers
|
||||
become: true
|
||||
roles:
|
||||
- gpu_model_storage
|
||||
23
playbooks/setup_logging.yml
Normal file
23
playbooks/setup_logging.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
# Prerequisite (create manually before running):
|
||||
# kubectl create namespace monitoring
|
||||
# kubectl create secret generic grafana-admin-secret \
|
||||
# --from-literal=admin-user=admin \
|
||||
# --from-literal=admin-password='YOUR_PASSWORD' \
|
||||
# -n monitoring
|
||||
#
|
||||
# Required CI/CD variables:
|
||||
# LOKI_MINIO_PASSWORD — password for the loki MinIO user (created by this playbook)
|
||||
# GRAFANA_ADMIN_PASSWORD — set in grafana-admin-secret above (not read by Ansible)
|
||||
- name: Deploy PLG logging stack (Loki + Alloy + Grafana)
|
||||
hosts: manager_nodes
|
||||
become: false
|
||||
vars:
|
||||
loki_minio_password: "{{ lookup('env', 'LOKI_MINIO_PASSWORD') }}"
|
||||
pre_tasks:
|
||||
- name: Preflight | Assert LOKI_MINIO_PASSWORD is set
|
||||
ansible.builtin.assert:
|
||||
that: loki_minio_password | length > 0
|
||||
fail_msg: "LOKI_MINIO_PASSWORD env var must be set"
|
||||
roles:
|
||||
- logging
|
||||
11
playbooks/setup_longhorn.yml
Normal file
11
playbooks/setup_longhorn.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
- name: Longhorn | Prepare cluster nodes (OS-level)
|
||||
hosts: k8s_cluster
|
||||
become: true
|
||||
roles:
|
||||
- longhorn_prereqs
|
||||
|
||||
- name: Longhorn | Annotate nodes and install via Helm
|
||||
hosts: manager_nodes
|
||||
roles:
|
||||
- longhorn
|
||||
6
playbooks/setup_manager.yml
Normal file
6
playbooks/setup_manager.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: Setup Kubernetes manager nodes
|
||||
hosts: manager_nodes
|
||||
gather_facts: true
|
||||
roles:
|
||||
- k8s_manager
|
||||
6
playbooks/setup_minio.yml
Normal file
6
playbooks/setup_minio.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: MinIO | Deploy object storage
|
||||
hosts: manager_nodes
|
||||
become: false
|
||||
roles:
|
||||
- minio
|
||||
6
playbooks/setup_sealed_secrets.yml
Normal file
6
playbooks/setup_sealed_secrets.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: Deploy Sealed Secrets (kubeseal controller + CLI)
|
||||
hosts: manager_nodes
|
||||
become: true
|
||||
roles:
|
||||
- sealed_secrets
|
||||
29
playbooks/setup_traefik.yml
Normal file
29
playbooks/setup_traefik.yml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
- name: Traefik | Open firewall ports on workers
|
||||
hosts: workers
|
||||
become: true
|
||||
tasks:
|
||||
- name: Firewall | Open Traefik service port range
|
||||
ansible.posix.firewalld:
|
||||
port: "10000-10999/tcp"
|
||||
permanent: true
|
||||
state: enabled
|
||||
immediate: true
|
||||
|
||||
- name: Firewall | Trust CNI interfaces
|
||||
ansible.posix.firewalld:
|
||||
zone: trusted
|
||||
interface: "{{ item }}"
|
||||
permanent: true
|
||||
state: enabled
|
||||
immediate: true
|
||||
loop:
|
||||
- flannel.1
|
||||
- cni0
|
||||
|
||||
- name: Traefik | Install via Helm and configure routes
|
||||
hosts: manager_nodes
|
||||
vars_files:
|
||||
- "{{ playbook_dir }}/../inventory/prod/group_vars/traefik.yml"
|
||||
roles:
|
||||
- traefik
|
||||
6
playbooks/setup_worker_plane.yml
Normal file
6
playbooks/setup_worker_plane.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: Setup Kubernetes worker nodes
|
||||
hosts: workers
|
||||
gather_facts: true
|
||||
roles:
|
||||
- k8s_worker
|
||||
8
requirements.yml
Normal file
8
requirements.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
collections:
|
||||
- name: ansible.posix
|
||||
version: ">=1.5.0"
|
||||
- name: community.general
|
||||
version: ">=9.0.0"
|
||||
- name: community.crypto
|
||||
version: ">=2.10.0"
|
||||
496
research/apps/hd-portal.md
Normal file
496
research/apps/hd-portal.md
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
# Перенос HD Portal (device-diagnostic) в Kubernetes
|
||||
|
||||
**Репозиторий:** `it-dept/device-diagnostic` на `gitlab.gigacoms.info`
|
||||
**Публичное название:** HD Portal
|
||||
**Текущая платформа:** Docker Swarm (нода `sandbox`)
|
||||
**Целевая платформа:** Kubernetes (кластер `k8s_infrastructure`)
|
||||
|
||||
---
|
||||
|
||||
## Что делает приложение
|
||||
|
||||
HD Portal — внутренний веб-портал для инженеров техподдержки. Позволяет:
|
||||
|
||||
1. **Найти абонента** по номеру договора через REST API BGBilling (`bill.gigacoms.ru`)
|
||||
2. **Подключиться к сетевому оборудованию** абонента по SSH или Telnet и выполнить диагностические команды
|
||||
3. **Сохранить результаты** диагностики в PostgreSQL
|
||||
4. **Создать заявку** в BGERP/BGCRM (`hd.gigacoms.ru`) с описанием проблемы
|
||||
5. Веб-интерфейс (Thymeleaf + Bootstrap) + REST API с API-Key аутентификацией (Swagger UI)
|
||||
|
||||
### Поддерживаемое оборудование
|
||||
|
||||
| Тип | Коды из биллинга | Протокол |
|
||||
|---|---|---|
|
||||
| BDCom | 14, 16, 119, 122 | SSH/Telnet |
|
||||
| DLink | 9, 49 | SSH/Telnet |
|
||||
| SNR | 17, 25 | SSH/Telnet |
|
||||
| TPLink | 65, 70 | SSH/Telnet |
|
||||
| ZTE EPON | 32 | SSH/Telnet |
|
||||
| ZTE GPON | 107 | SSH/Telnet |
|
||||
| СКАТ (BRAS) | 0 | SSH/Telnet |
|
||||
|
||||
---
|
||||
|
||||
## Технический стек
|
||||
|
||||
| Компонент | Версия |
|
||||
|---|---|
|
||||
| Java | 17 |
|
||||
| Spring Boot | 3.2.4 |
|
||||
| Spring Security | form login + API key filter |
|
||||
| Thymeleaf | шаблонизатор UI |
|
||||
| Flyway | миграции схемы БД (9 миграций) |
|
||||
| PostgreSQL | 15 (своя БД приложения) |
|
||||
| MariaDB 10.2 | внешняя БД BGERP (read-only: пользователи, группы) |
|
||||
| sshj 0.35.0 | SSH-подключения к оборудованию |
|
||||
| commons-net 3.9.0 | Telnet-подключения |
|
||||
| springdoc-openapi 2.5.0 | Swagger UI (`/swagger-ui.html`) |
|
||||
| образ | `eclipse-temurin:17-jre-alpine` |
|
||||
|
||||
---
|
||||
|
||||
## Текущая архитектура (Docker Swarm)
|
||||
|
||||
```
|
||||
Swarm (нода: sandbox, 10.203.0.211)
|
||||
│
|
||||
├── Service: diagnostic-app (replicas: 1)
|
||||
│ ├── Image: reg.gitlab.gigacoms.info/it-dept/device-diagnostic:latest
|
||||
│ ├── Port: 8181 → 8080 (ingress mode)
|
||||
│ ├── Network: diagnostic-network + nginx-proxy_nginx-network
|
||||
│ ├── JVM: -Xmx512m -Xms256m
|
||||
│ └── Env: APP_DATASOURCE_*, BGERP_DATASOURCE_*, BGERP_API_*
|
||||
│
|
||||
└── Service: diagnostic-db (replicas: 1)
|
||||
├── Image: postgres:15-alpine
|
||||
├── Volume: /mnt/swarm_quorum/diagnostic/postgresql/data → /var/lib/postgresql/data
|
||||
└── Network: diagnostic-network
|
||||
```
|
||||
|
||||
**Внешние зависимости приложения:**
|
||||
|
||||
| Система | Адрес | Тип | Назначение |
|
||||
|---|---|---|---|
|
||||
| BGBilling REST API | `https://bill.gigacoms.ru/bgbilling/...` | HTTPS | Данные по договору абонента |
|
||||
| BGERP REST API | `https://hd.gigacoms.ru/` | HTTPS | Создание заявок, сообщений |
|
||||
| BGERP MariaDB | `10.203.0.221:3306/bgcrm` | TCP | Аутентификация пользователей |
|
||||
| Сетевые устройства | LAN `10.x.x.x` | SSH/Telnet | Диагностические команды |
|
||||
|
||||
---
|
||||
|
||||
## Ключевые наблюдения для миграции
|
||||
|
||||
### 1. Приложение stateless — подходит для Deployment
|
||||
|
||||
Само приложение не хранит состояния между запросами. HTTP-сессии in-memory (стандарт Spring).
|
||||
Единственное состояние — PostgreSQL. Подходит для `Deployment` с 1 репликой (масштабирование возможно при наличии shared session store, но не нужно сейчас).
|
||||
|
||||
### 2. PostgreSQL — StatefulSet с Longhorn PVC
|
||||
|
||||
Текущий bind-mount `/mnt/swarm_quorum/diagnostic/postgresql/data` → `PersistentVolumeClaim` на Longhorn. Данные переносятся стандартным `pg_dump` / `pg_restore`.
|
||||
|
||||
### 3. Credentials hardcoded в коде — блокер
|
||||
|
||||
В `ContractInfoService.java` жёстко прописаны:
|
||||
```java
|
||||
private String apiUrl = "https://bill.gigacoms.ru/bgbilling/aiDmUnxl6KC8R1ELG4QJDPDm3IPn6V";
|
||||
private String apiUser = "sbersalute";
|
||||
private String apiPassword = "ULW2KmPd6bqc";
|
||||
```
|
||||
|
||||
Перед миграцией **обязательно** перенести эти значения в env-переменные (`BGBILLING_API_URL`, `BGBILLING_API_USER`, `BGBILLING_API_PASSWORD`) через `@Value` и `application.properties`. Без этого K8s Secret не поможет.
|
||||
|
||||
### 4. Пароли пользователей хранятся открытым текстом
|
||||
|
||||
`SecurityConfig` использует `NoOpPasswordEncoder`. Пользователи логинятся с паролями из BGERP MariaDB. Это работает, но является техническим долгом. Не блокирует миграцию.
|
||||
|
||||
### 5. Сетевой доступ к оборудованию LAN
|
||||
|
||||
Приложение SSH/Telnet-ается к коммутаторам и BRAS во внутренней сети (`10.x.x.x`). K8s-воркер (`10.203.0.96`) находится в той же сети — дополнительных маршрутов не нужно. **Важно:** flannel-интерфейсы (`flannel.1`, `cni0`) должны быть в `trusted` зоне firewalld на мастере и воркере (это уже обеспечено существующей инфраструктурой).
|
||||
|
||||
### 6. Nginx → Traefik
|
||||
|
||||
Сейчас приложение за Nginx reverse proxy (Swarm network `nginx-proxy_nginx-network`). В K8s — Traefik IngressRoute. UI и API требуют аутентификации, поэтому BasicAuth на уровне Traefik не нужен (приложение само управляет доступом).
|
||||
|
||||
### 7. CI: этап `mkdir` через SSH становится не нужным
|
||||
|
||||
Шаг `mkdir` в CI создаёт директорию `/mnt/swarm_quorum/diagnostic` на Swarm-хосте по SSH. В K8s PVC создаётся декларативно. Этап `mkdir` из `.gitlab-ci.yml` нужно убрать или заменить на `kubectl apply`.
|
||||
|
||||
---
|
||||
|
||||
## Целевая архитектура в Kubernetes
|
||||
|
||||
```
|
||||
Namespace: hd-portal
|
||||
│
|
||||
├── Secret: hd-portal-env
|
||||
│ └── все переменные окружения приложения
|
||||
│
|
||||
├── Secret: hd-portal-registry-pull (docker-registry)
|
||||
│
|
||||
├── PersistentVolumeClaim: hd-portal-postgres-data
|
||||
│ └── Longhorn, RWO, 5Gi
|
||||
│
|
||||
├── StatefulSet: hd-portal-postgres
|
||||
│ ├── Image: postgres:15-alpine
|
||||
│ └── Volume: hd-portal-postgres-data → /var/lib/postgresql/data
|
||||
│
|
||||
├── Service: hd-portal-postgres (ClusterIP, 5432)
|
||||
│
|
||||
├── Deployment: hd-portal
|
||||
│ ├── Image: reg.gitlab.gigacoms.info/it-dept/device-diagnostic:latest
|
||||
│ ├── replicas: 1
|
||||
│ ├── resources: requests 256Mi/200m, limits 768Mi/1
|
||||
│ └── Env from Secret: hd-portal-env
|
||||
│
|
||||
├── Service: hd-portal (ClusterIP, 8080)
|
||||
│
|
||||
└── Traefik IngressRoute: порт 10003 → hd-portal:8080
|
||||
(без BasicAuth — приложение имеет собственную аутентификацию)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Манифесты
|
||||
|
||||
### Namespace
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: hd-portal
|
||||
```
|
||||
|
||||
### Secret — переменные приложения
|
||||
|
||||
Создаётся вручную (не хранится в Git):
|
||||
|
||||
```bash
|
||||
kubectl create secret generic hd-portal-env \
|
||||
--from-literal=APP_DATASOURCE_URL="jdbc:postgresql://hd-portal-postgres:5432/diagnostic" \
|
||||
--from-literal=APP_DATASOURCE_USERNAME="postgres" \
|
||||
--from-literal=APP_DATASOURCE_PASSWORD="<пароль>" \
|
||||
--from-literal=BGERP_DATASOURCE_URL="jdbc:mariadb://10.203.0.221/bgcrm?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Europe/Moscow" \
|
||||
--from-literal=BGERP_DATASOURCE_USERNAME="user.portal" \
|
||||
--from-literal=BGERP_DATASOURCE_PASSWORD="<пароль>" \
|
||||
--from-literal=BGERP_API_URL="https://hd.gigacoms.ru/" \
|
||||
--from-literal=BGERP_API_USERNAME="autobot" \
|
||||
--from-literal=BGERP_API_PASSWORD="<пароль>" \
|
||||
--from-literal=BGBILLING_API_URL="https://bill.gigacoms.ru/bgbilling/<токен>" \
|
||||
--from-literal=BGBILLING_API_USER="sbersalute" \
|
||||
--from-literal=BGBILLING_API_PASSWORD="<пароль>" \
|
||||
--from-literal=SPRING_PROFILES_ACTIVE="prod" \
|
||||
--from-literal=JAVA_OPTS="-Xmx512m -Xms256m -Duser.timezone=Europe/Moscow" \
|
||||
--from-literal=TZ="Europe/Moscow" \
|
||||
-n hd-portal
|
||||
```
|
||||
|
||||
> Строка `BGBILLING_API_URL` и `BGBILLING_API_USER/PASSWORD` — только после исправления `ContractInfoService.java`.
|
||||
|
||||
### Secret для pull из GitLab Registry
|
||||
|
||||
```bash
|
||||
kubectl create secret docker-registry hd-portal-registry-pull \
|
||||
--docker-server=reg.gitlab.gigacoms.info \
|
||||
--docker-username=<deploy_token_user> \
|
||||
--docker-password=<deploy_token_password> \
|
||||
-n hd-portal
|
||||
```
|
||||
|
||||
### PVC для PostgreSQL
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: hd-portal-postgres-data
|
||||
namespace: hd-portal
|
||||
spec:
|
||||
storageClassName: longhorn
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
```
|
||||
|
||||
### StatefulSet — PostgreSQL
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: hd-portal-postgres
|
||||
namespace: hd-portal
|
||||
spec:
|
||||
serviceName: hd-portal-postgres
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: hd-portal-postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: hd-portal-postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:15-alpine
|
||||
env:
|
||||
- name: POSTGRES_DB
|
||||
value: diagnostic
|
||||
- name: POSTGRES_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hd-portal-env
|
||||
key: APP_DATASOURCE_USERNAME
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: hd-portal-env
|
||||
key: APP_DATASOURCE_PASSWORD
|
||||
- name: TZ
|
||||
value: Europe/Moscow
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["pg_isready", "-U", "postgres"]
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
resources:
|
||||
requests:
|
||||
memory: 128Mi
|
||||
cpu: 100m
|
||||
limits:
|
||||
memory: 512Mi
|
||||
cpu: 500m
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: hd-portal-postgres-data
|
||||
```
|
||||
|
||||
### Service — PostgreSQL
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: hd-portal-postgres
|
||||
namespace: hd-portal
|
||||
spec:
|
||||
selector:
|
||||
app: hd-portal-postgres
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
clusterIP: None # headless для StatefulSet
|
||||
```
|
||||
|
||||
### Deployment — приложение
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: hd-portal
|
||||
namespace: hd-portal
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: hd-portal
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: hd-portal
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: hd-portal-registry-pull
|
||||
containers:
|
||||
- name: app
|
||||
image: reg.gitlab.gigacoms.info/it-dept/device-diagnostic:latest
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: hd-portal-env
|
||||
resources:
|
||||
requests:
|
||||
memory: 256Mi
|
||||
cpu: 200m
|
||||
limits:
|
||||
memory: 768Mi
|
||||
cpu: "1"
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /actuator/health
|
||||
port: 8080
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /actuator/health
|
||||
port: 8080
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 30
|
||||
```
|
||||
|
||||
### Service — приложение
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: hd-portal
|
||||
namespace: hd-portal
|
||||
spec:
|
||||
selector:
|
||||
app: hd-portal
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
```
|
||||
|
||||
### Traefik IngressRoute
|
||||
|
||||
Добавить в `inventory/prod/group_vars/traefik.yml`:
|
||||
|
||||
```yaml
|
||||
traefik_port_map:
|
||||
# ... существующие записи ...
|
||||
- name: hd-portal # ≤15 символов
|
||||
port: 10003
|
||||
namespace: hd-portal
|
||||
service: hd-portal
|
||||
servicePort: 8080
|
||||
basicauth:
|
||||
enabled: false # приложение имеет собственную форму входа
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Необходимые изменения в коде (до миграции)
|
||||
|
||||
### Обязательно: вынести BGBilling credentials из кода
|
||||
|
||||
Файл: `src/main/java/ru/gigacoms/net/diagnostic/bgbilling/ContractInfoService.java`
|
||||
|
||||
**Было:**
|
||||
```java
|
||||
private String apiUrl = "https://bill.gigacoms.ru/bgbilling/aiDmUnxl6KC8R1ELG4QJDPDm3IPn6V";
|
||||
private String apiUser = "sbersalute";
|
||||
private String apiPassword = "ULW2KmPd6bqc";
|
||||
```
|
||||
|
||||
**Стало:**
|
||||
```java
|
||||
@Value("${bgbilling.api.url}")
|
||||
private String apiUrl;
|
||||
|
||||
@Value("${bgbilling.api.user}")
|
||||
private String apiUser;
|
||||
|
||||
@Value("${bgbilling.api.password}")
|
||||
private String apiPassword;
|
||||
```
|
||||
|
||||
И в `application.properties`:
|
||||
```properties
|
||||
bgbilling.api.url=${BGBILLING_API_URL}
|
||||
bgbilling.api.user=${BGBILLING_API_USER}
|
||||
bgbilling.api.password=${BGBILLING_API_PASSWORD}
|
||||
```
|
||||
|
||||
### Желательно: вынести API-ключи из application.properties в Secret
|
||||
|
||||
Сейчас `app.api.keys` указаны прямо в `application.properties`. При K8s-деплое лучше передавать через env-переменную:
|
||||
```properties
|
||||
app.api.keys=${APP_API_KEYS}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Перенос данных PostgreSQL
|
||||
|
||||
```bash
|
||||
# 1. Дамп с Swarm-ноды (sandbox)
|
||||
ssh root@10.203.0.211 "docker exec <postgres_container_id> \
|
||||
pg_dump -U postgres diagnostic" > diagnostic_dump.sql
|
||||
|
||||
# 2. Создать PVC и запустить StatefulSet (применить манифесты)
|
||||
|
||||
# 3. Восстановить дамп в K8s PostgreSQL
|
||||
kubectl exec -n hd-portal hd-portal-postgres-0 -- \
|
||||
psql -U postgres -d diagnostic < diagnostic_dump.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Изменения в CI/CD
|
||||
|
||||
Убрать из `.gitlab-ci.yml` этап `mkdir` — он создавал директории на Swarm-хосте по SSH. В K8s PVC управляется декларативно.
|
||||
|
||||
Опционально: добавить этап `deploy` для обновления образа в K8s после push:
|
||||
```bash
|
||||
kubectl set image deployment/hd-portal app=reg.gitlab.gigacoms.info/it-dept/device-diagnostic:$CI_COMMIT_SHA -n hd-portal
|
||||
```
|
||||
|
||||
Или через Flux — после push в Registry Flux Image Automation обновит манифест автоматически (если настроен ImageRepository + ImagePolicy).
|
||||
|
||||
---
|
||||
|
||||
## Интеграция с Flux
|
||||
|
||||
Манифесты размещаются в `k8s/k8s-fleet`:
|
||||
|
||||
```
|
||||
clusters/production/hd-portal/
|
||||
├── namespace.yaml
|
||||
├── pvc.yaml
|
||||
├── statefulset-postgres.yaml
|
||||
├── service-postgres.yaml
|
||||
├── deployment.yaml
|
||||
└── service.yaml
|
||||
```
|
||||
|
||||
Secrets (`hd-portal-env`, `hd-portal-registry-pull`) создаются вручную и **не хранятся в Git**.
|
||||
|
||||
Traefik-маршрут добавляется через Ansible (`setup_traefik.yml`) при изменении `traefik_port_map`.
|
||||
|
||||
---
|
||||
|
||||
## Риски и ограничения
|
||||
|
||||
| Риск | Оценка | Митигация |
|
||||
|---|---|---|
|
||||
| BGBilling credentials в коде | **высокий** | Обязательно исправить до миграции (см. секцию выше) |
|
||||
| `NoOpPasswordEncoder` (plaintext пароли) | средний | Технический долг, не блокирует миграцию |
|
||||
| SSH/Telnet к сетевым устройствам из pod | низкий | Воркер в той же LAN, маршрутизация работает |
|
||||
| Доступность BGERP MariaDB (`10.203.0.221`) | низкий | Прямой TCP, не зависит от K8s |
|
||||
| Длительные SSH-сессии к оборудованию | низкий | Диагностика занимает секунды, не минуты |
|
||||
| Flyway при старте — PostgreSQL может быть не готов | средний | `readinessProbe` на DB + `initialDelaySeconds: 30` на app |
|
||||
| Потеря данных при переносе PostgreSQL | средний | Сделать дамп прямо перед переключением |
|
||||
|
||||
---
|
||||
|
||||
## План миграции
|
||||
|
||||
1. **Исправить `ContractInfoService.java`** — вынести credentials в env-переменные, собрать новый образ.
|
||||
2. **Создать deploy token** для `it-dept/device-diagnostic` (scope: `read_registry`).
|
||||
3. **Сделать дамп PostgreSQL** с ноды `sandbox`.
|
||||
4. **Применить манифесты** через Flux или `kubectl apply`:
|
||||
Namespace → PVC → StatefulSet postgres → Service postgres → Deployment → Service
|
||||
5. **Создать секреты вручную**:
|
||||
- `hd-portal-env`
|
||||
- `hd-portal-registry-pull`
|
||||
6. **Восстановить дамп** в K8s PostgreSQL.
|
||||
7. **Проверить запуск** — убедиться в `/actuator/health` и доступности UI.
|
||||
8. **Добавить Traefik-маршрут** (`traefik_port_map`, порт 10003), запустить `setup_traefik.yml`.
|
||||
9. **Остановить Swarm-сервис** `diagnostic-app` и `diagnostic-db` после успешной проверки.
|
||||
10. **Удалить этап `mkdir`** из `.gitlab-ci.yml`.
|
||||
361
research/apps/rkn-list-poller.md
Normal file
361
research/apps/rkn-list-poller.md
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
# Перенос rkn-list-poller в Kubernetes
|
||||
|
||||
**Репозиторий:** `docker/rkn-list-poller` на `gitlab.gigacoms.info`
|
||||
**Текущая платформа:** Docker Swarm
|
||||
**Целевая платформа:** Kubernetes (кластер `k8s_infrastructure`)
|
||||
|
||||
---
|
||||
|
||||
## Что делает приложение
|
||||
|
||||
`rkn-list-poller` — пакетный PHP-воркер для выгрузки реестров заблокированных ресурсов через SOAP-сервис Роскомнадзора (`vigruzki.rkn.gov.ru`).
|
||||
|
||||
**Три скрипта, один алгоритм:**
|
||||
|
||||
1. Читает `request.xml` и `request.xml.sig` из примонтированной директории.
|
||||
2. Отправляет SOAP-запрос (`sendRequest`), получает `$request_code`.
|
||||
3. Опрашивает сервис каждые 3 минуты (`getResult` / `getResultSocResources`) до готовности.
|
||||
4. Записывает ZIP-архив дампа в ту же директорию.
|
||||
|
||||
| Скрипт | SOAP-метод | Выходной файл | Расписание cron |
|
||||
|---|---|---|---|
|
||||
| `rkn_get_black_list.php` | `getResult` | `dump_black.zip` | `0 * * * *` (каждый час в :00) |
|
||||
| `rkn_get_soc_list.php` | `getResultSocResources` | `dump_soc.zip` | `30 * * * *` (каждый час в :30) |
|
||||
| `rkn_get_list.php` | `getResult` | `dump.zip` | не в активном cron |
|
||||
|
||||
---
|
||||
|
||||
## Текущая архитектура (Docker Swarm)
|
||||
|
||||
```
|
||||
Docker Swarm (manager node)
|
||||
│
|
||||
└── Service: rkn-poller (replicas: 1)
|
||||
├── Image: reg.gitlab.gigacoms.info/docker/rkn-list-poller:main-latest
|
||||
├── Entrypoint: cron -f (PID 1, foreground)
|
||||
│ ├── 0 */1 * * * → php /app/rkn_get_black_list.php
|
||||
│ └── 30 */1 * * * → php /app/rkn_get_soc_list.php
|
||||
└── Volume: /mnt/swarm_quorum/rkn-poller → /data/rezult
|
||||
├── request.xml (генерируется rknutils.jar, обновляется вручную)
|
||||
├── request.xml.sig (GOST-подпись, обновляется вручную)
|
||||
├── dump_black.zip (выход)
|
||||
├── dump_soc.zip (выход)
|
||||
└── rkn.log (журнал)
|
||||
```
|
||||
|
||||
**Особенности образа:**
|
||||
- База: `rnix/openssl-gost:latest` (Debian Stretch, EOL) — нужен для GOST-криптографии при TLS с `vigruzki.rkn.gov.ru`
|
||||
- PHP 7.0, php-soap, php-xml
|
||||
- Российские корневые сертификаты Минцифры (5 штук в `certs/`) добавлены в хранилище системы и в `php.ini`
|
||||
- Лимит памяти PHP: `-1` (без ограничений) — дамп может занять сотни МБ
|
||||
|
||||
---
|
||||
|
||||
## Ключевые наблюдения для миграции
|
||||
|
||||
### 1. Приложение — пакетный джоб, не сервис
|
||||
|
||||
Контейнер сейчас держит внутри себя `cron` и работает постоянно. В Kubernetes это антипаттерн.
|
||||
**PHP-скрипты — это одиночные запуски с выходом**: `php /app/rkn_get_black_list.php` запускается, ждёт ответа РКН, сохраняет файл, завершается.
|
||||
|
||||
Это идеально соответствует `CronJob`. Нужды в Deployment нет.
|
||||
|
||||
### 2. Время выполнения непредсказуемо
|
||||
|
||||
Скрипт опрашивает РКН каждые 3 минуты в цикле. РКН может отвечать от 5 минут до нескольких часов. Нужно устанавливать `activeDeadlineSeconds` с большим запасом (6 часов).
|
||||
|
||||
### 3. Конкурентный запуск опасен
|
||||
|
||||
Если предыдущий джоб ещё работает (РКН медленно отвечает), новый запуск перезапишет тот же `dump_black.zip`. Нужен `concurrencyPolicy: Forbid`.
|
||||
|
||||
### 4. Персистентное хранилище — единственная точка состояния
|
||||
|
||||
Из Swarm-тома в директорию `/data/rezult` нужно перенести:
|
||||
- `request.xml` и `request.xml.sig` — **входные файлы, которые нельзя потерять**. Обновляются вручную при смене сертификата оператора. Логично хранить как Secret.
|
||||
- Выходные ZIP-архивы — в PVC.
|
||||
|
||||
### 5. Образ менять не нужно
|
||||
|
||||
Образ `rnix/openssl-gost` специфичен для GOST-криптографии и уже работает. CI/CD уже публикует его в GitLab Registry. **Менять образ не нужно** — только изменить способ запуска (убрать cron, запускать скрипт напрямую).
|
||||
|
||||
> **Технический долг:** Debian Stretch достиг EOL в 2022. `rnix/openssl-gost` — заброшенный образ. Рекомендуется проверить наличие более свежих альтернатив (например, `openssl-gost` на базе Debian Bookworm), но это отдельная задача, не блокирующая миграцию.
|
||||
|
||||
---
|
||||
|
||||
## Целевая архитектура в Kubernetes
|
||||
|
||||
```
|
||||
Namespace: rkn-poller
|
||||
│
|
||||
├── Secret: rkn-request-files
|
||||
│ ├── request.xml (base64)
|
||||
│ └── request.xml.sig (base64)
|
||||
│
|
||||
├── PersistentVolumeClaim: rkn-data (Longhorn, RWO, 5Gi)
|
||||
│ └── /data/rezult/
|
||||
│ ├── dump_black.zip
|
||||
│ ├── dump_soc.zip
|
||||
│ └── rkn.log
|
||||
│
|
||||
├── CronJob: rkn-black-list
|
||||
│ ├── schedule: "0 * * * *"
|
||||
│ ├── concurrencyPolicy: Forbid
|
||||
│ ├── activeDeadlineSeconds: 21600
|
||||
│ ├── command: php /app/rkn_get_black_list.php
|
||||
│ └── volumes: Secret → /data/rezult (request файлы), PVC → /data/rezult (выход)
|
||||
│
|
||||
└── CronJob: rkn-soc-list
|
||||
├── schedule: "30 * * * *"
|
||||
├── concurrencyPolicy: Forbid
|
||||
├── activeDeadlineSeconds: 21600
|
||||
├── command: php /app/rkn_get_soc_list.php
|
||||
└── volumes: то же самое
|
||||
```
|
||||
|
||||
**Почему два тома в одну директорию?**
|
||||
Оба пути монтирования (`/data/rezult`) можно разрешить через `subPath` в PVC — тогда Secret с файлами запроса проецируется поверх PVC не заменяя его содержимое. Либо использовать initContainer для копирования файлов из Secret в PVC перед запуском основного контейнера (более явный вариант).
|
||||
|
||||
---
|
||||
|
||||
## Манифесты
|
||||
|
||||
### Namespace
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: rkn-poller
|
||||
```
|
||||
|
||||
### Secret с файлами запроса
|
||||
|
||||
Создаётся **вручную** на manager-ноде (не хранится в Git):
|
||||
|
||||
```bash
|
||||
kubectl create secret generic rkn-request-files \
|
||||
--from-file=request.xml=/path/to/request.xml \
|
||||
--from-file=request.xml.sig=/path/to/request.xml.sig \
|
||||
-n rkn-poller
|
||||
```
|
||||
|
||||
При смене сертификата оператора:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic rkn-request-files \
|
||||
--from-file=request.xml=/path/to/new/request.xml \
|
||||
--from-file=request.xml.sig=/path/to/new/request.xml.sig \
|
||||
-n rkn-poller \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
```
|
||||
|
||||
### Secret для pull из GitLab Registry
|
||||
|
||||
```bash
|
||||
kubectl create secret docker-registry rkn-registry-pull \
|
||||
--docker-server=reg.gitlab.gigacoms.info \
|
||||
--docker-username=<gitlab_deploy_token_user> \
|
||||
--docker-password=<gitlab_deploy_token_password> \
|
||||
-n rkn-poller
|
||||
```
|
||||
|
||||
### PersistentVolumeClaim
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: rkn-data
|
||||
namespace: rkn-poller
|
||||
spec:
|
||||
storageClassName: longhorn
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
```
|
||||
|
||||
### CronJob: rkn-black-list
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: rkn-black-list
|
||||
namespace: rkn-poller
|
||||
spec:
|
||||
schedule: "0 * * * *"
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 3
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
activeDeadlineSeconds: 21600 # 6 часов: РКН может отвечать долго
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
imagePullSecrets:
|
||||
- name: rkn-registry-pull
|
||||
initContainers:
|
||||
- name: copy-request-files
|
||||
image: busybox:1.36
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- cp /secrets/request.xml /data/rezult/request.xml &&
|
||||
cp /secrets/request.xml.sig /data/rezult/request.xml.sig
|
||||
volumeMounts:
|
||||
- name: request-secret
|
||||
mountPath: /secrets
|
||||
readOnly: true
|
||||
- name: rkn-data
|
||||
mountPath: /data/rezult
|
||||
containers:
|
||||
- name: poller
|
||||
image: reg.gitlab.gigacoms.info/docker/rkn-list-poller:main-latest
|
||||
command: ["php", "/app/rkn_get_black_list.php"]
|
||||
env:
|
||||
- name: RKN_DATA_DIR
|
||||
value: /data/rezult
|
||||
resources:
|
||||
requests:
|
||||
memory: 256Mi
|
||||
cpu: 100m
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 500m
|
||||
volumeMounts:
|
||||
- name: rkn-data
|
||||
mountPath: /data/rezult
|
||||
volumes:
|
||||
- name: request-secret
|
||||
secret:
|
||||
secretName: rkn-request-files
|
||||
- name: rkn-data
|
||||
persistentVolumeClaim:
|
||||
claimName: rkn-data
|
||||
```
|
||||
|
||||
### CronJob: rkn-soc-list
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: rkn-soc-list
|
||||
namespace: rkn-poller
|
||||
spec:
|
||||
schedule: "30 * * * *"
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 3
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
activeDeadlineSeconds: 21600
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
imagePullSecrets:
|
||||
- name: rkn-registry-pull
|
||||
initContainers:
|
||||
- name: copy-request-files
|
||||
image: busybox:1.36
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- cp /secrets/request.xml /data/rezult/request.xml &&
|
||||
cp /secrets/request.xml.sig /data/rezult/request.xml.sig
|
||||
volumeMounts:
|
||||
- name: request-secret
|
||||
mountPath: /secrets
|
||||
readOnly: true
|
||||
- name: rkn-data
|
||||
mountPath: /data/rezult
|
||||
containers:
|
||||
- name: poller
|
||||
image: reg.gitlab.gigacoms.info/docker/rkn-list-poller:main-latest
|
||||
command: ["php", "/app/rkn_get_soc_list.php"]
|
||||
env:
|
||||
- name: RKN_DATA_DIR
|
||||
value: /data/rezult
|
||||
resources:
|
||||
requests:
|
||||
memory: 256Mi
|
||||
cpu: 100m
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 500m
|
||||
volumeMounts:
|
||||
- name: rkn-data
|
||||
mountPath: /data/rezult
|
||||
volumes:
|
||||
- name: request-secret
|
||||
secret:
|
||||
secretName: rkn-request-files
|
||||
- name: rkn-data
|
||||
persistentVolumeClaim:
|
||||
claimName: rkn-data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Интеграция с Flux
|
||||
|
||||
Манифесты размещаются в fleet-репозитории `k8s/k8s-fleet` (путь: `clusters/production/rkn-poller/`):
|
||||
|
||||
```
|
||||
clusters/production/rkn-poller/
|
||||
├── namespace.yaml
|
||||
├── pvc.yaml
|
||||
├── cronjob-black-list.yaml
|
||||
└── cronjob-soc-list.yaml
|
||||
```
|
||||
|
||||
Secret-ресурсы (`rkn-request-files`, `rkn-registry-pull`) **не хранятся в Git** — создаются вручную один раз.
|
||||
|
||||
Flux автоматически применит PVC и CronJob после push в fleet-репозиторий.
|
||||
|
||||
---
|
||||
|
||||
## Замечания по firewall и сети
|
||||
|
||||
- `vigruzki.rkn.gov.ru` — внешний HTTPS-хост. Pods в кластере имеют выход в интернет через worker-ноды — дополнительных правил не нужно.
|
||||
- NetworkPolicy не требуется: поды не принимают входящих соединений.
|
||||
- Для ГОСТ-TLS нужны российские корневые сертификаты — они уже вшиты в образ, ничего дополнительно настраивать не нужно.
|
||||
|
||||
---
|
||||
|
||||
## Риски и ограничения
|
||||
|
||||
| Риск | Оценка | Митигация |
|
||||
|---|---|---|
|
||||
| `rnix/openssl-gost` на Debian Stretch EOL | средний | Образ работает, обновление — отдельная задача. Изолирован в контейнере. |
|
||||
| Долгий старт пода (pull большого образа) | низкий | Образ уже в локальном Registry, время pull минимально |
|
||||
| РКН возвращает ошибку — pod упадёт, cron не перезапустит до следующего часа | средний | `restartPolicy: OnFailure` сделает retry. Можно добавить `backoffLimit: 3` в Job |
|
||||
| PVC `ReadWriteOnce` — только одна нода | принимается | Оба CronJob работают последовательно и на одной ноде (single-worker кластер) |
|
||||
| `request.xml.sig` истёк (сертификат оператора) — нет алертинга | средний | Добавить мониторинг по коду завершения Job (future work) |
|
||||
|
||||
---
|
||||
|
||||
## План миграции
|
||||
|
||||
1. **Создать deploy token** в GitLab (`docker/rkn-list-poller` → Settings → Repository → Deploy tokens, scope `read_registry`)
|
||||
2. **Применить манифесты** через Flux или `kubectl apply`:
|
||||
- Namespace → PVC → CronJob × 2
|
||||
3. **Создать секреты вручную** на manager-ноде:
|
||||
- `rkn-registry-pull` (registry credentials)
|
||||
- `rkn-request-files` (request.xml + request.xml.sig из текущего Swarm-тома)
|
||||
4. **Запустить джобы вручную** для проверки:
|
||||
```bash
|
||||
kubectl create job rkn-black-list-test \
|
||||
--from=cronjob/rkn-black-list -n rkn-poller
|
||||
kubectl logs -f -n rkn-poller -l job-name=rkn-black-list-test
|
||||
```
|
||||
5. **Убедиться, что `dump_black.zip` обновился** в PVC:
|
||||
```bash
|
||||
kubectl run check --rm -it --image=busybox \
|
||||
--overrides='{"spec":{"volumes":[{"name":"d","persistentVolumeClaim":{"claimName":"rkn-data"}}],"containers":[{"name":"c","image":"busybox","command":["ls","-lh","/data"],"volumeMounts":[{"name":"d","mountPath":"/data"}]}]}}' \
|
||||
-n rkn-poller
|
||||
```
|
||||
6. **Остановить Swarm-сервис** `rkn` после подтверждения работы.
|
||||
1152
research/flux.md
Normal file
1152
research/flux.md
Normal file
File diff suppressed because it is too large
Load diff
1275
research/logging/plg.md
Normal file
1275
research/logging/plg.md
Normal file
File diff suppressed because it is too large
Load diff
453
research/longhorn.md
Normal file
453
research/longhorn.md
Normal file
|
|
@ -0,0 +1,453 @@
|
|||
# Longhorn — исследование для развёртывания в кластере
|
||||
|
||||
## Что такое Longhorn
|
||||
|
||||
Longhorn — распределённое блочное хранилище для Kubernetes (CNCF-проект, изначально Rancher).
|
||||
Предоставляет PersistentVolume через CSI-драйвер с репликацией данных между нодами, инкрементальными снапшотами и встроенным бэкапом.
|
||||
|
||||
---
|
||||
|
||||
## На каких нодах нужны диски
|
||||
|
||||
Longhorn Manager запускается как **DaemonSet на нодах кластера**. Каждая нода, где работает Longhorn Manager, может хранить реплики томов.
|
||||
|
||||
**Применительно к этому кластеру:**
|
||||
|
||||
| Нода | Группа | Роль в Longhorn |
|
||||
|---|---|---|
|
||||
| k8s-master-01 (10.203.0.97) | control_plane | Может участвовать (тейнт уже снят), но нежелательно в production |
|
||||
| k8s-worker-NN | workers | **Основные ноды для хранилища** — диски должны быть здесь |
|
||||
| k8s-manager-01 (10.203.0.92) | manager_nodes | Не входит в кластер, диски не нужны |
|
||||
|
||||
**Вывод:** диски нужны на **worker-нодах**. Желательно не смешивать роль control plane и хранилища.
|
||||
|
||||
---
|
||||
|
||||
## Минимальное количество нод
|
||||
|
||||
| Режим | Нод с дисками | Репликация | Отказоустойчивость |
|
||||
|---|---|---|---|
|
||||
| Dev / тест | 1 | 1 | Нет |
|
||||
| Minimum HA | **3** | 2 | Выдерживает отказ 1 ноды |
|
||||
| Production HA | **3** | 3 | Выдерживает отказ 2 нод |
|
||||
| Extended HA | 5+ | 3 | Рекомендуется для критичных данных |
|
||||
|
||||
### Почему именно 3 ноды — минимум для стабильной работы
|
||||
|
||||
Longhorn по умолчанию размещает реплики тома на **разных нодах** (replica node anti-affinity).
|
||||
- При `replication=3` требуются 3 разные ноды, иначе Longhorn деградирует том или откажет в создании.
|
||||
- При `replication=2` достаточно 2 нод, но потеря любой из них оставит данные без резерва — том продолжит работу, но уязвим.
|
||||
- 3 ноды + `replication=3` — классическая кворум-схема: можно потерять 1 ноду и данные остаются защищёнными.
|
||||
|
||||
**Практические варианты для этого кластера:**
|
||||
- **Рекомендуется:** 3 worker-ноды + control plane (без хранилища)
|
||||
- **Допустимо (временно):** 2 worker-ноды + control plane с Longhorn — итого 3 ноды с дисками, но смешение ролей нежелательно
|
||||
|
||||
---
|
||||
|
||||
## Минимальное количество дисков на ноде
|
||||
|
||||
- **Минимум: 1 диск на ноду.** Longhorn работает с одним диском.
|
||||
- **Рекомендуется: 1 выделенный диск** (отдельно от ОС-диска `/dev/sda`).
|
||||
- Longhorn поддерживает **несколько дисков на одной ноде** — реплики балансируются между ними автоматически.
|
||||
|
||||
### ОС-диск vs выделенный диск
|
||||
|
||||
| Параметр | ОС-диск | Выделенный диск |
|
||||
|---|---|---|
|
||||
| Изоляция от системы | Нет | Да |
|
||||
| `minimalAvailableStoragePercentage` | ≥25% | 10% |
|
||||
| Риск DiskPressure | Высокий | Низкий |
|
||||
| Рекомендация | Только dev/тест | Production |
|
||||
|
||||
**Минимальная жизнеспособная production-конфигурация:**
|
||||
```
|
||||
3 worker-ноды × 1 выделенный диск = 3 диска суммарно
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Принципы репликации
|
||||
|
||||
Longhorn реплицирует данные **посинхронно** (synchronous replication) между репликами при записи.
|
||||
|
||||
```
|
||||
Pod (write) → Longhorn Engine → Replica 1 (node-1 /dev/sdb)
|
||||
→ Replica 2 (node-2 /dev/sdb)
|
||||
→ Replica 3 (node-3 /dev/sdb)
|
||||
```
|
||||
|
||||
- Запись подтверждается только когда все реплики записали данные.
|
||||
- При отказе реплики Longhorn автоматически перестраивает её на другой ноде (Rebuild).
|
||||
- Значение `replication` задаётся на уровне StorageClass или отдельного тома.
|
||||
|
||||
### Рекомендуемые настройки anti-affinity
|
||||
|
||||
| Настройка | Значение | Смысл |
|
||||
|---|---|---|
|
||||
| `replicaNodeLevelSoftAntiAffinity` | `false` | Запрет размещения 2 реплик на одной ноде (hard rule) |
|
||||
| `replicaDiskLevelSoftAntiAffinity` | `true` | Разрешить несколько реплик на одной ноде, но на разных дисках |
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites — что нужно установить на все ноды кластера
|
||||
|
||||
### Пакеты (Rocky Linux 9)
|
||||
|
||||
```bash
|
||||
dnf install -y iscsi-initiator-utils nfs-utils cryptsetup
|
||||
```
|
||||
|
||||
| Пакет | Зачем |
|
||||
|---|---|
|
||||
| `iscsi-initiator-utils` | iSCSI-стек для подключения томов |
|
||||
| `nfs-utils` | NFS-поддержка для RWX-томов и бэкапов |
|
||||
| `cryptsetup` | LUKS2-шифрование томов (опционально) |
|
||||
|
||||
### Сервисы
|
||||
|
||||
```bash
|
||||
systemctl enable --now iscsid
|
||||
```
|
||||
|
||||
### Kernel modules
|
||||
|
||||
```
|
||||
iscsi_tcp — iSCSI over TCP
|
||||
dm_crypt — Device-mapper шифрование
|
||||
```
|
||||
|
||||
Добавить в `/etc/modules-load.d/longhorn.conf`:
|
||||
```
|
||||
iscsi_tcp
|
||||
dm_crypt
|
||||
```
|
||||
|
||||
### Утилиты (должны быть в PATH)
|
||||
|
||||
`bash`, `curl`, `findmnt`, `grep`, `awk`, `blkid`, `lsblk` — как правило, уже присутствуют на Rocky Linux 9.
|
||||
|
||||
---
|
||||
|
||||
## SELinux workaround (Rocky Linux 9 — обязательно)
|
||||
|
||||
Rocky Linux 9 с `container-selinux > 2.189.0` блокирует `iscsiadm` через SELinux, что приводит к бесконечному циклу attach/detach томов.
|
||||
|
||||
### Ручной патч (на каждой ноде)
|
||||
|
||||
```bash
|
||||
echo '(allow iscsid_t self (capability (dac_override)))' > /tmp/local_longhorn.cil
|
||||
semodule -vi /tmp/local_longhorn.cil
|
||||
```
|
||||
|
||||
### Автоматически через DaemonSet (после установки Longhorn)
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/master/deploy/prerequisite/longhorn-iscsi-selinux-workaround.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Firewall (firewalld)
|
||||
|
||||
Добавить на ноды кластера:
|
||||
|
||||
| Порт | Протокол | Зачем |
|
||||
|---|---|---|
|
||||
| 2049 | tcp | NFS (для RWX томов и бэкапов) |
|
||||
| 111 | tcp/udp | portmapper (NFS) |
|
||||
|
||||
Порт 10250/tcp (kubelet API) уже открыт в существующей роли `k8s_control_plane`.
|
||||
|
||||
---
|
||||
|
||||
## Установка — Helm chart (рекомендуемый метод)
|
||||
|
||||
```bash
|
||||
helm repo add longhorn https://charts.longhorn.io
|
||||
helm repo update
|
||||
|
||||
helm install longhorn longhorn/longhorn \
|
||||
--namespace longhorn-system \
|
||||
--create-namespace \
|
||||
--version 1.7.2 \
|
||||
--set defaultSettings.defaultReplicaCount=3 \
|
||||
--set defaultSettings.replicaNodeLevelSoftAntiAffinity=false \
|
||||
--set defaultSettings.minimalAvailableStoragePercentage=10
|
||||
```
|
||||
|
||||
### Ключевые Helm-параметры
|
||||
|
||||
| Параметр | Рекомендуемое значение | Описание |
|
||||
|---|---|---|
|
||||
| `defaultSettings.defaultReplicaCount` | `3` | Реплик по умолчанию на новый том |
|
||||
| `defaultSettings.replicaNodeLevelSoftAntiAffinity` | `false` | Запрет реплик на одной ноде |
|
||||
| `defaultSettings.minimalAvailableStoragePercentage` | `10` (dedicated) / `25` (OS disk) | Резерв свободного места |
|
||||
| `defaultSettings.storageOverProvisioningPercentage` | `100` | Overprovisioning (200 = двойной запас) |
|
||||
|
||||
---
|
||||
|
||||
## Проверка готовности нод (preflight)
|
||||
|
||||
Longhorn предоставляет утилиту `longhornctl`:
|
||||
|
||||
```bash
|
||||
# Скачать
|
||||
curl -sSfL -o longhornctl https://github.com/longhorn/cli/releases/latest/download/longhornctl-linux-amd64
|
||||
chmod +x longhornctl
|
||||
|
||||
# Проверить prerequisites
|
||||
./longhornctl check preflight
|
||||
|
||||
# Автоматически установить зависимости
|
||||
./longhornctl install preflight
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## StorageClass после установки
|
||||
|
||||
Longhorn создаёт StorageClass `longhorn` автоматически:
|
||||
|
||||
```yaml
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: longhorn
|
||||
provisioner: driver.longhorn.io
|
||||
parameters:
|
||||
numberOfReplicas: "3"
|
||||
staleReplicaTimeout: "2880"
|
||||
fromBackup: ""
|
||||
reclaimPolicy: Delete
|
||||
volumeBindingMode: Immediate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Итоговые минимумы для production
|
||||
|
||||
| Параметр | Минимум (stable) | Рекомендуется |
|
||||
|---|---|---|
|
||||
| Нод с дисками | **3** | 3+ |
|
||||
| Дисков на ноду | **1** (выделенный) | 1–2 |
|
||||
| Репликация | **2** | 3 |
|
||||
| Пакеты | open-iscsi + nfs-utils | + cryptsetup |
|
||||
| SELinux патч | **Обязательно** на Rocky Linux 9 | — |
|
||||
| Отдельный диск от ОС | **Рекомендуется** | — |
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Масштабирование: добавление дисков и нод
|
||||
|
||||
### Сценарий A: добавление диска к существующей ноде
|
||||
|
||||
#### Как это работает
|
||||
|
||||
Longhorn не отслеживает диски автоматически — каждый новый диск нужно **явно зарегистрировать** через Longhorn UI или kubectl. После регистрации диск мгновенно становится доступен для новых томов.
|
||||
|
||||
#### Подготовка диска (на уровне ОС)
|
||||
|
||||
```bash
|
||||
# 1. Отформатировать (ext4 или xfs — обязательно extent-based)
|
||||
mkfs.xfs /dev/sdb
|
||||
|
||||
# 2. Создать точку монтирования
|
||||
mkdir -p /mnt/longhorn-disk2
|
||||
|
||||
# 3. Получить UUID (надёжнее device-name)
|
||||
blkid /dev/sdb
|
||||
|
||||
# 4. Добавить в /etc/fstab (UUID, nofail — обязательно)
|
||||
echo "UUID=<uuid> /mnt/longhorn-disk2 xfs defaults,nofail 0 2" >> /etc/fstab
|
||||
|
||||
# 5. Смонтировать
|
||||
mount -a
|
||||
```
|
||||
|
||||
> **Важно:** не использовать symlink в пути — Longhorn-поды не разрешают символические ссылки корректно.
|
||||
|
||||
#### Регистрация диска в Longhorn
|
||||
|
||||
**Через kubectl (рекомендуется для Ansible):**
|
||||
```bash
|
||||
kubectl patch nodes.longhorn.io <node-name> -n longhorn-system \
|
||||
--type merge \
|
||||
-p '{"spec":{"disks":{"disk2":{"path":"/mnt/longhorn-disk2","allowScheduling":true,"diskType":"filesystem"}}}}'
|
||||
```
|
||||
|
||||
**Через UI:** Nodes → выбрать ноду → Edit node and disks → Add Disk.
|
||||
|
||||
#### Автоматический ребалансинг реплик на новый диск?
|
||||
|
||||
**Нет.** Существующие реплики **не переезжают** автоматически на новый диск.
|
||||
|
||||
- Новый диск начинает использоваться только для **новых томов**.
|
||||
- Если включена функция `Replica Auto Balance` (режим `best-effort`), Longhorn постепенно перемещает часть реплик для выравнивания нагрузки между дисками.
|
||||
- Настройка: Longhorn UI → Settings → Replica Auto Balance → `best-effort`.
|
||||
|
||||
#### Сложности при добавлении диска
|
||||
|
||||
| Сложность | Причина | Решение |
|
||||
|---|---|---|
|
||||
| Диск не виден Longhorn | Не смонтирован до регистрации | `mount \| grep /mnt/longhorn-disk2` |
|
||||
| Ошибка дублирования | Тот же filesystem UUID уже в кластере | Проверить `lsblk -f` перед добавлением |
|
||||
| Disk не используется | `allowScheduling: false` | Включить через UI или patch |
|
||||
| Резерв места слишком велик | `storageReserved` ≥ объёму диска | Понизить до 10% от объёма |
|
||||
|
||||
#### Теги дисков (routing для специфичных воркеров)
|
||||
|
||||
Можно пометить диск тегом (например `ssd`, `fast`) и указывать его в StorageClass:
|
||||
```yaml
|
||||
# StorageClass
|
||||
parameters:
|
||||
diskSelector: "ssd"
|
||||
```
|
||||
Тогда реплики создаются только на дисках с нужными тегами.
|
||||
|
||||
---
|
||||
|
||||
### Сценарий B: добавление ноды к кластеру
|
||||
|
||||
#### Автоматическое обнаружение
|
||||
|
||||
Когда новая нода присоединяется к Kubernetes, **Longhorn обнаруживает её автоматически** через DaemonSet — никаких ручных действий не требуется. Longhorn-manager pod поднимается на новой ноде и создаёт Longhorn Node CR.
|
||||
|
||||
По умолчанию Longhorn сразу создаёт диск по пути из настройки `Default Data Path` (`/var/lib/longhorn`).
|
||||
|
||||
#### Проверка после появления ноды
|
||||
|
||||
```bash
|
||||
# Убедиться, что нода появилась в Longhorn
|
||||
kubectl get node.longhorn.io -n longhorn-system
|
||||
|
||||
# Проверить диск на ноде
|
||||
kubectl get node.longhorn.io <node-name> -n longhorn-system -o yaml
|
||||
# Ищем: spec.disks, status.diskStatus — должны быть schedulable: true
|
||||
```
|
||||
|
||||
#### Ребалансинг существующих реплик на новую ноду?
|
||||
|
||||
**По умолчанию — нет.** Существующие реплики не переезжают на новую ноду автоматически.
|
||||
|
||||
Чтобы включить ребалансинг:
|
||||
```
|
||||
Longhorn UI → Settings → Replica Auto Balance → best-effort
|
||||
```
|
||||
|
||||
| Режим | Поведение |
|
||||
|---|---|
|
||||
| `disabled` (по умолчанию) | Реплики не перемещаются |
|
||||
| `least-effort` | Минимальное перемещение — только для восстановления fault-tolerance |
|
||||
| `best-effort` | Равномерное распределение по всем нодам |
|
||||
|
||||
#### Ноды с Выделенными дисками (не `/var/lib/longhorn`)
|
||||
|
||||
Если диск подключён как `/dev/sdb` и смонтирован в `/mnt/longhorn`, нужно:
|
||||
1. Подготовить и смонтировать диск на новой ноде (аналогично Сценарию A).
|
||||
2. Зарегистрировать диск в Longhorn вручную или через аннотацию ноды (до или после присоединения к кластеру):
|
||||
|
||||
```bash
|
||||
kubectl annotate node <node-name> \
|
||||
node.longhorn.io/default-disks-config='[{"path":"/mnt/longhorn","allowScheduling":true}]'
|
||||
```
|
||||
|
||||
#### Сложности при добавлении ноды
|
||||
|
||||
| Сложность | Причина | Решение |
|
||||
|---|---|---|
|
||||
| Нода есть в K8s, но не в Longhorn | DaemonSet не запустился (taint, ресурсы) | Проверить `kubectl get pods -n longhorn-system -o wide` |
|
||||
| Нода есть в Longhorn, но не планирует реплики | `allowScheduling: false` или нет свободного места | Включить scheduling, проверить space |
|
||||
| Реплики не переехали | Auto Balance выключен | Включить `best-effort` |
|
||||
| Existing volumes не используют новую ноду | Replication count не обновлён | Обновить replica count (см. ниже) |
|
||||
| Prerequisites не установлены | Нет `iscsid`, `nfs-utils` | Запустить роль prereqs перед join |
|
||||
|
||||
> **Критично:** перед добавлением ноды к кластеру на ней **обязаны быть установлены** `iscsi-initiator-utils`, `nfs-utils` и применён SELinux-патч (для Rocky Linux 9). Иначе Longhorn Manager pod запустится, но тома не смогут монтироваться на этой ноде.
|
||||
|
||||
---
|
||||
|
||||
### Сценарий C: миграция 1 нода → 3 ноды (увеличение репликации)
|
||||
|
||||
#### Ситуация
|
||||
|
||||
Старт: 1 worker-нода, `replication=1` — данные есть, избыточности нет.
|
||||
Цель: 3 ноды, `replication=3` — полная HA.
|
||||
|
||||
#### Окно риска
|
||||
|
||||
Пока replica count увеличивается с 1 до 3, том находится в состоянии **Degraded**: старая реплика работает, новые перестраиваются. В этот момент:
|
||||
|
||||
- Том **доступен** для чтения и записи (без даунтайма).
|
||||
- Если нода со старой единственной репликой упадёт **во время перестройки** → **данные потеряны**.
|
||||
|
||||
Поэтому перед увеличением replica count — **сделать снапшот или бэкап**.
|
||||
|
||||
#### Процедура
|
||||
|
||||
```bash
|
||||
# 1. Убедиться, что новые ноды видны Longhorn
|
||||
kubectl get node.longhorn.io -n longhorn-system
|
||||
|
||||
# 2. Включить Auto Balance (опционально, для автоматического выравнивания)
|
||||
# Longhorn UI → Settings → Replica Auto Balance → best-effort
|
||||
|
||||
# 3. Обновить replica count для существующих томов
|
||||
kubectl patch volume <volume-name> -n longhorn-system \
|
||||
--type merge \
|
||||
-p '{"spec":{"numberOfReplicas":3}}'
|
||||
|
||||
# 4. Мониторинг перестройки
|
||||
kubectl get volume <volume-name> -n longhorn-system \
|
||||
-o jsonpath='{.status.state} {.status.replicaCount}'
|
||||
# Ожидаем: "healthy 3"
|
||||
```
|
||||
|
||||
#### Время перестройки (ориентир)
|
||||
|
||||
| Объём тома | Время rebuild |
|
||||
|---|---|
|
||||
| 10 GiB | 2–5 минут |
|
||||
| 100 GiB | 10–20 минут |
|
||||
| 1 TiB | 1–4 часа |
|
||||
|
||||
#### Типичные проблемы при переходе 1→3
|
||||
|
||||
| Проблема | Симптом | Решение |
|
||||
|---|---|---|
|
||||
| Том завис в Degraded | `replicaCount` не растёт > 1 часа | Проверить ноды: space, scheduling, taints |
|
||||
| Replica count остался 1 | `spec.numberOfReplicas=3`, но `status.replicaCount=1` | Новые ноды `unschedulable`? `kubectl get node.longhorn.io` |
|
||||
| Только 2 реплики создались | Третья нода без свободного места | Добавить диск или ноду с бо́льшим объёмом |
|
||||
| `Data Locality: guaranteed` мешает | Том не перемещается | Сменить на `best-effort` на период миграции |
|
||||
|
||||
#### Обновить StorageClass для новых томов
|
||||
|
||||
Изменить `numberOfReplicas` в StorageClass, чтобы все **новые** PVC создавались сразу с нужной репликацией:
|
||||
```bash
|
||||
kubectl patch storageclass longhorn \
|
||||
-p '{"parameters":{"numberOfReplicas":"3"}}'
|
||||
```
|
||||
|
||||
Это не влияет на уже существующие тома — только на новые.
|
||||
|
||||
---
|
||||
|
||||
### Итоговая таблица: сводка по масштабированию
|
||||
|
||||
| Действие | Автоматически? | Даунтайм? | Главный риск | Что сделать |
|
||||
|---|---|---|---|---|
|
||||
| Добавить диск на ноду | Нет (ручная регистрация) | Нет | Диск не смонтирован до регистрации | Смонтировать → зарегистрировать через kubectl/UI |
|
||||
| Добавить ноду в кластер | Да (DaemonSet) | Нет | Prerequisites не установлены | Запустить prereqs-роль до join |
|
||||
| Ребалансинг реплик | Нет (нужно включить) | Нет | Реплики остаются на старых нодах/дисках | Включить `Replica Auto Balance: best-effort` |
|
||||
| Поднять replica count 1→3 | Нет (вручную) | Нет (том в Degraded) | Потеря данных при падении ноды во время rebuild | Снапшот перед изменением, мониторинг rebuild |
|
||||
|
||||
---
|
||||
|
||||
## Источники
|
||||
|
||||
- [Longhorn Official Documentation 1.11](https://longhorn.io/docs/1.11.2/)
|
||||
- [Longhorn Installation Guide](https://longhorn.io/docs/1.11.2/deploy/install/)
|
||||
- [Longhorn Best Practices](https://longhorn.io/docs/1.11.2/best-practices/)
|
||||
- [Longhorn SELinux Troubleshooting](https://longhorn.io/kb/troubleshooting-volume-attachment-fails-due-to-selinux-denials/)
|
||||
- [Longhorn Multiple Disk Support](https://longhorn.io/docs/1.10.1/nodes-and-volumes/nodes/multidisk/)
|
||||
788
research/minio.md
Normal file
788
research/minio.md
Normal file
|
|
@ -0,0 +1,788 @@
|
|||
# MinIO в Kubernetes: объектное хранилище 1 ТБ с веб-интерфейсом
|
||||
|
||||
**Кластер:** Rocky Linux 9 · Kubernetes 1.33 · Flannel CNI
|
||||
**Текущая топология:** k8s-manager-01 (10.203.0.92) · k8s-master-01 (10.203.0.97) · k8s-worker-01 (10.203.0.96)
|
||||
**Дата исследования:** 2026-05-28
|
||||
**Проверено на кластере:** 2026-05-28 (kubectl + Longhorn API)
|
||||
|
||||
---
|
||||
|
||||
## Содержание
|
||||
|
||||
1. [Обзор и назначение](#1-обзор-и-назначение)
|
||||
2. [Режимы развёртывания](#2-режимы-развёртывания)
|
||||
3. [Планирование хранилища 1 ТБ](#3-планирование-хранилища-1-тб)
|
||||
4. [Helm-развёртывание (standalone)](#4-helm-развёртывание-standalone)
|
||||
5. [MinIO Console: веб-интерфейс](#5-minio-console-веб-интерфейс)
|
||||
6. [Интеграция с Traefik](#6-интеграция-с-traefik)
|
||||
7. [Безопасность: пользователи и политики](#7-безопасность-пользователи-и-политики)
|
||||
8. [Интеграция с Loki (S3 backend)](#8-интеграция-с-loki-s3-backend)
|
||||
9. [Ansible-роль и плейбук](#9-ansible-роль-и-плейбук)
|
||||
10. [Масштабирование до distributed-режима](#10-масштабирование-до-distributed-режима)
|
||||
11. [Итоги и рекомендации](#11-итоги-и-рекомендации)
|
||||
|
||||
---
|
||||
|
||||
## 1. Обзор и назначение
|
||||
|
||||
MinIO — S3-совместимое объектное хранилище. Работает в Kubernetes как StatefulSet или Deployment, предоставляет:
|
||||
|
||||
- **S3 API** (порт 9000) — совместим с любым клиентом AWS SDK, boto3, mc, s3cmd
|
||||
- **MinIO Console** (порт 9001) — встроенный веб-интерфейс: браузер объектов, управление пользователями, мониторинг
|
||||
|
||||
### Сценарии использования в текущем кластере
|
||||
|
||||
| Сценарий | Описание |
|
||||
|---|---|
|
||||
| Loki S3 backend | Хранилище chunks/index при переходе на distributed Loki |
|
||||
| Бэкапы баз данных | S3-target для pg_dump, mysqldump, Velero |
|
||||
| Артефакты CI/CD | GitLab Runner cache, артефакты сборки |
|
||||
| Файловое хранилище | Статика, медиафайлы приложений |
|
||||
| Резервные копии etcd | Автоматические снэпшоты control plane |
|
||||
|
||||
---
|
||||
|
||||
## 2. Режимы развёртывания
|
||||
|
||||
### Текущее состояние кластера (проверено)
|
||||
|
||||
```
|
||||
Longhorn-диски на k8s-worker-01:
|
||||
longhorn-disk1 /mnt/longhorn-disk1 ~4.09 TiB (свободно ~4.06 TiB) allowScheduling=true
|
||||
longhorn-disk2 /mnt/longhorn-disk2 ~4.09 TiB (свободно ~4.06 TiB) allowScheduling=true
|
||||
|
||||
k8s-master-01: диск из Longhorn удалён — нода без дисков, в планировании не участвует ✓
|
||||
|
||||
StorageClass longhorn (default):
|
||||
numberOfReplicas: 1 ✓
|
||||
allowVolumeExpansion: true
|
||||
createDefaultDiskLabeledNodes: true
|
||||
|
||||
Traefik hostPort (занято): 10001 (k8s-dashboard), 10002 (longhorn-ui)
|
||||
Traefik hostPort (свободно): 10003+ → MinIO: 10005 (API), 10006 (Console)
|
||||
```
|
||||
|
||||
### Standalone (текущий кластер — 1 worker)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ k8s-worker-01 (10.203.0.96) │
|
||||
│ │
|
||||
│ [MinIO Pod — StatefulSet 1 replica] │
|
||||
│ ├── порт 9000 (S3 API) │
|
||||
│ └── порт 9001 (Console UI) │
|
||||
│ ↓ │
|
||||
│ [PVC 1 TiB → StorageClass longhorn-minio] │
|
||||
│ [Longhorn → longhorn-disk1 (/mnt/longhorn-disk1, 4 TiB)] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓ ClusterIP
|
||||
┌─────────────────────┐
|
||||
│ Traefik │
|
||||
│ 10005 → API :9000 │
|
||||
│ 10006 → UI :9001 │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
**Ограничения standalone:** нет erasure coding. Потеря диска = потеря данных. Приемлемо при наличии Longhorn-снэпшотов по расписанию.
|
||||
|
||||
### SNMD — Single Node Multi-Drive (будущее)
|
||||
|
||||
Worker уже имеет два диска (~4 TiB каждый). MinIO поддерживает SNMD начиная с 4 дисков. При добавлении ещё двух дисков к worker-ноде можно перейти на SNMD без новых нод:
|
||||
|
||||
```
|
||||
MinIO SNMD, 4 диска по ~1 TiB (из доступных 4 TiB на каждом диске):
|
||||
- usable storage ≈ 2 TiB (EC:2 — паритет 50%)
|
||||
- допустимая потеря: 2 из 4 дисков
|
||||
```
|
||||
|
||||
### Distributed (4+ нод)
|
||||
|
||||
При добавлении worker-нод: 1 под MinIO на ноду, PVC через Longhorn на каждой. Требует минимум 4 пода (4 ноды) для полноценного erasure coding.
|
||||
|
||||
**Для текущего кластера** (1 worker, 2 диска) — только standalone.
|
||||
|
||||
---
|
||||
|
||||
## 3. Планирование хранилища 1 ТБ
|
||||
|
||||
### Реальная конфигурация дисков (проверено)
|
||||
|
||||
На `k8s-worker-01` уже настроены два Longhorn-диска:
|
||||
|
||||
| Диск | Путь | Всего | Свободно | Статус |
|
||||
|---|---|---|---|---|
|
||||
| `longhorn-disk1` | `/mnt/longhorn-disk1` | ~4.09 TiB | ~4.06 TiB | Ready, Schedulable |
|
||||
| `longhorn-disk2` | `/mnt/longhorn-disk2` | ~4.09 TiB | ~4.06 TiB | Ready, Schedulable |
|
||||
|
||||
Оба диска полностью свободны. PVC 1 TiB займёт ~25% одного диска, оставляя ~3 TiB в резерве на том же диске.
|
||||
|
||||
### Выделенный StorageClass для MinIO
|
||||
|
||||
Дефолтный `longhorn` (numberOfReplicas: 1) технически подойдёт, но для MinIO рекомендуется отдельный StorageClass по двум причинам:
|
||||
|
||||
- **reclaimPolicy: Retain** — при случайном удалении namespace или PVC данные на диске не уничтожаются (дефолтный Longhorn использует `Delete`)
|
||||
- **diskSelector** — позволяет зафиксировать MinIO на `longhorn-disk1`, оставив `longhorn-disk2` для других workload (Loki, бэкапы и т.д.)
|
||||
|
||||
```yaml
|
||||
# применить через роль minio/tasks/storageclass.yml
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: longhorn-minio
|
||||
provisioner: driver.longhorn.io
|
||||
reclaimPolicy: Retain # данные остаются при удалении PVC
|
||||
allowVolumeExpansion: true
|
||||
parameters:
|
||||
numberOfReplicas: "1"
|
||||
dataLocality: "best-effort"
|
||||
diskSelector: "minio" # тег только на longhorn-disk1
|
||||
fsType: "ext4"
|
||||
dataEngine: "v1"
|
||||
```
|
||||
|
||||
### Disk-тег для изоляции MinIO на disk1 (опционально)
|
||||
|
||||
Если нужно зафиксировать MinIO именно на `longhorn-disk1` (рекомендуется при нескольких workload):
|
||||
|
||||
```bash
|
||||
kubectl -n longhorn-system patch node.longhorn.io k8s-worker-01 --type=json -p='[
|
||||
{"op":"add","path":"/spec/disks/longhorn-disk1/tags","value":["minio"]}
|
||||
]'
|
||||
```
|
||||
|
||||
Без тега Longhorn выберет любой из двух дисков (disk1 или disk2) — это тоже корректно, просто менее предсказуемо.
|
||||
|
||||
### Репликация на уровне Longhorn
|
||||
|
||||
Сейчас: `numberOfReplicas: 1` — без репликации. При добавлении второго worker с Longhorn-дисками поднять до `2` — Longhorn начнёт реплицировать PVC MinIO между нодами без остановки пода.
|
||||
|
||||
> **Важно:** репликация Longhorn защищает от потери ноды, но **не заменяет** MinIO distributed mode. Longhorn реплицирует блочное устройство целиком, erasure coding MinIO работает на уровне объектов.
|
||||
|
||||
### Расчёт полезной ёмкости
|
||||
|
||||
Физический диск (`longhorn-disk1`) — ~4.09 TiB. MinIO получает PVC 1 TiB — остаток диска доступен для других PVC.
|
||||
|
||||
| Параметр | Значение |
|
||||
|---|---|
|
||||
| Физический диск | ~4.09 TiB (`longhorn-disk1`) |
|
||||
| PVC для MinIO | 1 TiB |
|
||||
| Overhead Longhorn + ext4 | ~2% |
|
||||
| Overhead MinIO (метаданные) | ~1% |
|
||||
| **Доступно для объектов** | **~990 Gi** |
|
||||
| Рекомендуемый порог заполнения | 80% → ~790 Gi |
|
||||
| Остаток на диске для других PVC | ~3.09 TiB |
|
||||
|
||||
MinIO по умолчанию отказывается принимать данные при заполнении > 95% (настраивается через `MINIO_STORAGE_CLASS_STANDARD`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Helm-развёртывание (standalone)
|
||||
|
||||
### Helm chart
|
||||
|
||||
Используется официальный chart `minio/minio` (не bitnami — он добавляет лишние зависимости).
|
||||
|
||||
```bash
|
||||
helm repo add minio https://charts.min.io/
|
||||
helm repo update
|
||||
```
|
||||
|
||||
### `minio-values.yml`
|
||||
|
||||
```yaml
|
||||
# roles/minio/files/minio-values.yml
|
||||
|
||||
## Режим: standalone
|
||||
mode: standalone
|
||||
|
||||
## Образ
|
||||
image:
|
||||
repository: quay.io/minio/minio
|
||||
tag: RELEASE.2025-04-22T22-12-26Z # фиксированная версия
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
## Корневые учётные данные (переопределяются через Secret)
|
||||
existingSecret: minio-root-credentials
|
||||
|
||||
## Хранилище
|
||||
persistence:
|
||||
enabled: true
|
||||
storageClass: longhorn-minio # выделенный SC: numberOfReplicas=1, только worker-диски
|
||||
accessMode: ReadWriteOnce
|
||||
size: 1Ti
|
||||
|
||||
## Ресурсы пода
|
||||
resources:
|
||||
requests:
|
||||
memory: 512Mi
|
||||
cpu: 250m
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 1000m
|
||||
|
||||
## Привязка к worker-ноде (не запускать на control plane)
|
||||
nodeSelector:
|
||||
node-role.kubernetes.io/control-plane: "" # исключить
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: node-role.kubernetes.io/control-plane
|
||||
operator: DoesNotExist
|
||||
|
||||
## Сервисы
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 9000
|
||||
|
||||
consoleService:
|
||||
type: ClusterIP
|
||||
port: 9001
|
||||
|
||||
## Buckets создаются автоматически при старте
|
||||
buckets:
|
||||
- name: loki-chunks
|
||||
policy: none
|
||||
purge: false
|
||||
- name: backups
|
||||
policy: none
|
||||
purge: false
|
||||
- name: artifacts
|
||||
policy: none
|
||||
purge: false
|
||||
|
||||
## Пользователи (пароли — через Secret, задаются ниже)
|
||||
users:
|
||||
- accessKey: loki
|
||||
existingSecret: minio-user-loki
|
||||
existingSecretKey: secretKey
|
||||
policy: readwrite
|
||||
- accessKey: backup
|
||||
existingSecret: minio-user-backup
|
||||
existingSecretKey: secretKey
|
||||
policy: readwrite
|
||||
|
||||
## Политики для бакетов
|
||||
policies:
|
||||
- name: loki-policy
|
||||
statements:
|
||||
- resources:
|
||||
- "arn:aws:s3:::loki-chunks"
|
||||
- "arn:aws:s3:::loki-chunks/*"
|
||||
actions:
|
||||
- "s3:GetObject"
|
||||
- "s3:PutObject"
|
||||
- "s3:DeleteObject"
|
||||
- "s3:ListBucket"
|
||||
|
||||
## Метрики (если Prometheus есть)
|
||||
metrics:
|
||||
serviceMonitor:
|
||||
enabled: false # включить когда появится Prometheus
|
||||
|
||||
## MinIO Console — веб-интерфейс
|
||||
consoleIngress:
|
||||
enabled: false # используем Traefik напрямую через ClusterIP
|
||||
|
||||
## Окружение MinIO
|
||||
environment:
|
||||
MINIO_BROWSER_REDIRECT_URL: "http://10.203.0.96:10006" # URL Console через Traefik
|
||||
MINIO_STORAGE_CLASS_STANDARD: "EC:0" # standalone: без erasure coding
|
||||
MINIO_UPDATE: "off" # отключить автообновление
|
||||
```
|
||||
|
||||
### Secret с корневыми учётными данными
|
||||
|
||||
```bash
|
||||
kubectl create secret generic minio-root-credentials \
|
||||
--from-literal=rootUser=minioadmin \
|
||||
--from-literal=rootPassword='СИЛЬНЫЙ_ПАРОЛЬ_МИНИМУМ_8_СИМВОЛОВ' \
|
||||
-n minio
|
||||
```
|
||||
|
||||
### Установка
|
||||
|
||||
```bash
|
||||
kubectl create namespace minio
|
||||
|
||||
helm upgrade --install minio minio/minio \
|
||||
--namespace minio \
|
||||
--version 5.4.0 \
|
||||
--values roles/minio/files/minio-values.yml \
|
||||
--wait
|
||||
```
|
||||
|
||||
### Проверка
|
||||
|
||||
```bash
|
||||
# Статус пода
|
||||
kubectl get pod -n minio
|
||||
|
||||
# Логи
|
||||
kubectl logs -n minio -l app=minio
|
||||
|
||||
# Проброс порта для быстрой проверки
|
||||
kubectl port-forward -n minio svc/minio 9000:9000 &
|
||||
kubectl port-forward -n minio svc/minio-console 9001:9001 &
|
||||
|
||||
# Проверка S3 API через mc (MinIO Client)
|
||||
mc alias set local http://localhost:9000 minioadmin ПАРОЛЬ
|
||||
mc ls local
|
||||
mc admin info local
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. MinIO Console: веб-интерфейс
|
||||
|
||||
MinIO Console — встроенный React-интерфейс, запускается в том же поде на порту 9001. Отдельная установка не нужна начиная с MinIO RELEASE.2021-07-08.
|
||||
|
||||
### Возможности Console
|
||||
|
||||
| Раздел | Функции |
|
||||
|---|---|
|
||||
| Object Browser | Навигация по бакетам, загрузка/скачивание файлов, просмотр метаданных |
|
||||
| Buckets | Создание бакетов, версионирование, lifecycle policies, replication |
|
||||
| Identity → Users | Создание пользователей, назначение политик |
|
||||
| Identity → Groups | Группировка пользователей |
|
||||
| Identity → Policies | Редактор IAM-политик (JSON) |
|
||||
| Monitoring | Дашборд загрузки, IOPS, throughput в реальном времени |
|
||||
| Logs | Потоковый просмотр логов MinIO в браузере |
|
||||
| Audit | Журнал операций (включается через `MINIO_AUDIT_WEBHOOK_*`) |
|
||||
|
||||
### Настройка адреса Console
|
||||
|
||||
MinIO Console проверяет `MINIO_BROWSER_REDIRECT_URL` при формировании redirect после логина. Без правильного значения Console вернёт 401 или redirect на неверный URL.
|
||||
|
||||
```yaml
|
||||
# В minio-values.yml — уже задано выше
|
||||
environment:
|
||||
MINIO_BROWSER_REDIRECT_URL: "http://10.203.0.96:10006"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Интеграция с Traefik
|
||||
|
||||
Два порта в `traefik_port_map`:
|
||||
- `10005` — MinIO S3 API (для клиентов, boto3, mc)
|
||||
- `10006` — MinIO Console (веб-интерфейс)
|
||||
|
||||
### Добавить в `inventory/prod/group_vars/traefik.yml`
|
||||
|
||||
```yaml
|
||||
traefik_port_map:
|
||||
# ... существующие записи ...
|
||||
|
||||
- name: minio-api
|
||||
description: "MinIO S3 API"
|
||||
port: 10005
|
||||
backend:
|
||||
namespace: minio
|
||||
service: minio
|
||||
port: 9000
|
||||
scheme: http
|
||||
basicauth:
|
||||
enabled: false # MinIO использует собственную аутентификацию (AWS Signature v4)
|
||||
|
||||
- name: minio-console
|
||||
description: "MinIO Console (Web UI)"
|
||||
port: 10006
|
||||
backend:
|
||||
namespace: minio
|
||||
service: minio-console
|
||||
port: 9001
|
||||
scheme: http
|
||||
basicauth:
|
||||
enabled: false # MinIO Console имеет собственный логин
|
||||
```
|
||||
|
||||
> **Важно:** BasicAuth от Traefik + MinIO Console не совместимы — браузер не может пройти двойную аутентификацию. MinIO Console защищён своим логином, этого достаточно.
|
||||
|
||||
После добавления запустить `setup_traefik.yml`.
|
||||
|
||||
### Адреса после развёртывания
|
||||
|
||||
| Сервис | URL | Назначение |
|
||||
|---|---|---|
|
||||
| MinIO S3 API | `http://10.203.0.96:10005` | Внешний доступ клиентов |
|
||||
| MinIO Console | `http://10.203.0.96:10006` | Веб-интерфейс администратора |
|
||||
| MinIO API (внутри кластера) | `http://minio.minio.svc.cluster.local:9000` | Для подов кластера |
|
||||
| MinIO Console (внутри кластера) | `http://minio-console.minio.svc.cluster.local:9001` | — |
|
||||
|
||||
### Настройка mc (MinIO Client) для внешнего доступа
|
||||
|
||||
```bash
|
||||
# На manager-ноде или локально
|
||||
mc alias set prod http://10.203.0.96:10005 minioadmin ПАРОЛЬ
|
||||
|
||||
# Проверка
|
||||
mc ls prod
|
||||
mc admin info prod
|
||||
mc du prod/loki-chunks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Безопасность: пользователи и политики
|
||||
|
||||
### Принцип минимальных привилегий
|
||||
|
||||
Не используйте root-учётные данные в приложениях. Для каждого сервиса — отдельный пользователь с ограниченной политикой.
|
||||
|
||||
### Создание пользователей через mc
|
||||
|
||||
```bash
|
||||
# Пользователь для Loki (только свой бакет)
|
||||
mc admin user add prod loki ПАРОЛЬ_LOKI
|
||||
|
||||
mc admin policy create prod loki-policy /dev/stdin <<'EOF'
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:*"],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::loki-chunks",
|
||||
"arn:aws:s3:::loki-chunks/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
mc admin policy attach prod loki-policy --user loki
|
||||
|
||||
# Пользователь для бэкапов (только запись в backups)
|
||||
mc admin user add prod backup ПАРОЛЬ_BACKUP
|
||||
|
||||
mc admin policy create prod backup-policy /dev/stdin <<'EOF'
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:PutObject", "s3:GetObject", "s3:ListBucket"],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::backups",
|
||||
"arn:aws:s3:::backups/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
mc admin policy attach prod backup-policy --user backup
|
||||
```
|
||||
|
||||
### Lifecycle policy: автоудаление старых объектов
|
||||
|
||||
```bash
|
||||
# Удалять объекты в backups старше 30 дней
|
||||
mc ilm rule add --expire-days 30 prod/backups
|
||||
|
||||
# Удалять незавершённые multipart uploads старше 7 дней
|
||||
mc ilm rule add --expire-days 7 --noncurrent-expire-days 7 prod/backups
|
||||
```
|
||||
|
||||
### Версионирование бакетов (защита от случайного удаления)
|
||||
|
||||
```bash
|
||||
# Включить версионирование
|
||||
mc version enable prod/backups
|
||||
|
||||
# Посмотреть все версии объекта
|
||||
mc ls --versions prod/backups/db-dump.sql.gz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Интеграция с Loki (S3 backend)
|
||||
|
||||
При переходе Loki на distributed-режим (см. [PLG исследование](../logging/plg.md)) MinIO становится S3 backend для хранения chunks и index.
|
||||
|
||||
### Loki values для S3 backend
|
||||
|
||||
```yaml
|
||||
# roles/logging/templates/loki-values.yml.j2 (distributed режим)
|
||||
loki:
|
||||
storage:
|
||||
type: s3
|
||||
s3:
|
||||
endpoint: http://minio.minio.svc.cluster.local:9000
|
||||
region: us-east-1 # MinIO игнорирует region, но поле обязательно
|
||||
bucketnames: loki-chunks
|
||||
access_key_id: loki
|
||||
secret_access_key: "{{ loki_minio_password }}"
|
||||
insecure: true # http (не https)
|
||||
s3forcepathstyle: true # обязательно для MinIO
|
||||
|
||||
schemaConfig:
|
||||
configs:
|
||||
- from: "2024-01-01"
|
||||
store: tsdb
|
||||
object_store: s3 # ← было filesystem
|
||||
schema: v13
|
||||
index:
|
||||
prefix: loki_index_
|
||||
period: 24h
|
||||
```
|
||||
|
||||
### Secret для Loki в namespace monitoring
|
||||
|
||||
```bash
|
||||
kubectl create secret generic loki-minio-secret \
|
||||
--from-literal=AWS_ACCESS_KEY_ID=loki \
|
||||
--from-literal=AWS_SECRET_ACCESS_KEY='ПАРОЛЬ_LOKI' \
|
||||
-n monitoring
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Ansible-роль и плейбук
|
||||
|
||||
### Структура роли `roles/minio/`
|
||||
|
||||
```
|
||||
roles/minio/
|
||||
├── defaults/main.yml
|
||||
├── tasks/
|
||||
│ ├── main.yml
|
||||
│ ├── namespace.yml — создать namespace minio
|
||||
│ ├── secrets.yml — создать Secrets из vault/переменных
|
||||
│ ├── helm.yml — helm upgrade --install minio
|
||||
│ └── mc.yml — настройка mc alias, пользователей, политик, lifecycle
|
||||
├── files/
|
||||
│ └── minio-values.yml
|
||||
└── templates/
|
||||
└── minio-values.yml.j2
|
||||
```
|
||||
|
||||
### `defaults/main.yml`
|
||||
|
||||
```yaml
|
||||
minio_namespace: minio
|
||||
minio_chart_version: "5.4.0" # minio/minio chart
|
||||
minio_image_tag: "RELEASE.2025-04-22T22-12-26Z"
|
||||
|
||||
minio_storage_class: longhorn-minio # выделенный SC, не дефолтный longhorn (numberOfReplicas=3)
|
||||
minio_storage_size: 1Ti
|
||||
|
||||
minio_console_url: "http://10.203.0.96:10006"
|
||||
minio_api_external_url: "http://10.203.0.96:10005"
|
||||
|
||||
# Имена Secrets (значения создаются вручную через kubectl)
|
||||
minio_root_secret: minio-root-credentials
|
||||
|
||||
minio_buckets:
|
||||
- loki-chunks
|
||||
- backups
|
||||
- artifacts
|
||||
|
||||
minio_resources_requests_memory: "512Mi"
|
||||
minio_resources_limits_memory: "2Gi"
|
||||
```
|
||||
|
||||
### `tasks/helm.yml`
|
||||
|
||||
```yaml
|
||||
- name: MinIO | Add Helm repo
|
||||
kubernetes.core.helm_repository:
|
||||
name: minio
|
||||
repo_url: https://charts.min.io/
|
||||
|
||||
- name: MinIO | Deploy via Helm
|
||||
kubernetes.core.helm:
|
||||
name: minio
|
||||
chart_ref: minio/minio
|
||||
chart_version: "{{ minio_chart_version }}"
|
||||
release_namespace: "{{ minio_namespace }}"
|
||||
create_namespace: true
|
||||
values: "{{ lookup('template', 'minio-values.yml.j2') | from_yaml }}"
|
||||
wait: true
|
||||
wait_condition:
|
||||
type: Ready
|
||||
status: "True"
|
||||
timeout: "10m0s"
|
||||
```
|
||||
|
||||
### `playbooks/setup_minio.yml`
|
||||
|
||||
```yaml
|
||||
---
|
||||
- name: Deploy MinIO object storage
|
||||
hosts: manager_nodes
|
||||
become: false
|
||||
roles:
|
||||
- minio
|
||||
```
|
||||
|
||||
### `.gitlab-ci.yml` — новый job
|
||||
|
||||
```yaml
|
||||
setup/minio:
|
||||
stage: setup
|
||||
script:
|
||||
- ansible-playbook -i inventory/prod playbooks/setup_minio.yml
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
resource_group: production
|
||||
```
|
||||
|
||||
### Переменные GitLab CI/CD (добавить)
|
||||
|
||||
| Переменная | Описание |
|
||||
|---|---|
|
||||
| `MINIO_ROOT_PASSWORD` | Пароль root-пользователя MinIO |
|
||||
| `MINIO_LOKI_PASSWORD` | Пароль пользователя loki |
|
||||
| `MINIO_BACKUP_PASSWORD` | Пароль пользователя backup |
|
||||
|
||||
---
|
||||
|
||||
## 10. Масштабирование
|
||||
|
||||
### Этапы роста: Longhorn и MinIO вместе
|
||||
|
||||
Реальное текущее состояние (проверено): worker имеет 2 диска по ~4 TiB, оба пустые. Это даёт несколько путей масштабирования без добавления нод:
|
||||
|
||||
| Этап | Кластер | Longhorn | MinIO |
|
||||
|---|---|---|---|
|
||||
| **Сейчас** | 1 worker, 2 диска по 4 TiB | `longhorn` SC: `numberOfReplicas: 1`, master без дисков ✓ | standalone, PVC 1 TiB на disk1 |
|
||||
| +2 диска к worker | 1 worker, 4 диска | 4 PVC по 1 TiB, `numberOfReplicas: 1` | **SNMD**: 4 drives, EC:2, ~2 TiB usable |
|
||||
| +1 worker с дисками | 2 workers | Поднять `numberOfReplicas: 2` в `longhorn-minio` | standalone + Longhorn-репликация между нодами |
|
||||
| +3 workers (4 total) | 4 workers | `numberOfReplicas: 2` | **Distributed**: 4 пода, EC:2 |
|
||||
|
||||
### Шаг 1: добавление второго worker (без пересоздания MinIO)
|
||||
|
||||
После добавления `k8s-worker-02` в кластер:
|
||||
|
||||
```bash
|
||||
# Убедиться что Longhorn видит новую ноду и диск
|
||||
kubectl get nodes -o wide
|
||||
kubectl get nodes.longhorn.io -n longhorn-system
|
||||
|
||||
# Обновить numberOfReplicas у StorageClass (или через Longhorn UI)
|
||||
kubectl edit storageclass longhorn
|
||||
# numberOfReplicas: "1" → "2"
|
||||
|
||||
# Обновить replicas у существующего PVC MinIO
|
||||
kubectl -n longhorn-system edit volume <minio-pvc-volume-name>
|
||||
# spec.numberOfReplicas: 1 → 2
|
||||
# Longhorn начнёт ребалансировку в фоне, MinIO продолжает работать
|
||||
```
|
||||
|
||||
### Шаг 2: SNMD — 4 диска на одной ноде (реалистичный следующий шаг)
|
||||
|
||||
Worker уже имеет 2 диска (~4 TiB каждый). При добавлении ещё 2 дисков можно перейти на SNMD **без новых нод**:
|
||||
|
||||
```yaml
|
||||
# minio-values.yml — SNMD режим (4 диска на k8s-worker-01)
|
||||
mode: distributed # в MinIO SNMD тоже использует mode: distributed
|
||||
replicas: 1 # 1 нода
|
||||
drivesPerNode: 4 # 4 PVC на под
|
||||
|
||||
persistence:
|
||||
storageClass: longhorn-minio
|
||||
size: 1Ti # 4 × 1 TiB = 4 TiB raw → ~2 TiB usable (EC:2)
|
||||
```
|
||||
|
||||
При SNMD Longhorn создаёт 4 отдельных PVC (по одному на каждый логический диск MinIO). `numberOfReplicas: 1` в StorageClass — MinIO EC уже обеспечивает отказоустойчивость.
|
||||
|
||||
### Шаг 3: distributed (4+ workers)
|
||||
|
||||
```yaml
|
||||
# minio-values.yml при 4 worker-нодах
|
||||
mode: distributed
|
||||
replicas: 4 # 4 пода на 4 нодах
|
||||
drivesPerNode: 1 # 1 Longhorn PVC на под
|
||||
|
||||
persistence:
|
||||
storageClass: longhorn-minio
|
||||
size: 1Ti # 4 × 1 TiB raw → ~2 TiB usable (EC:2)
|
||||
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchLabels:
|
||||
app: minio
|
||||
topologyKey: kubernetes.io/hostname
|
||||
```
|
||||
|
||||
В distributed-режиме Longhorn предоставляет отдельный PVC каждому поду MinIO. Репликацию Longhorn для distributed оставить на `numberOfReplicas: 1` — MinIO EC уже обеспечивает отказоустойчивость.
|
||||
|
||||
### Миграция standalone → distributed
|
||||
|
||||
MinIO **не поддерживает** in-place миграцию. Процедура:
|
||||
|
||||
```
|
||||
1. Запустить distributed MinIO рядом (namespace minio-dist)
|
||||
2. mc mirror minio-standalone minio-distributed --preserve --watch
|
||||
3. Дождаться синхронизации всех объектов (mc du для сверки объёмов)
|
||||
4. Переключить Traefik (изменить service в IngressRoute) на новый сервис
|
||||
5. Обновить endpoint в configs Loki, бэкапов и других клиентов
|
||||
6. Подождать 24-48 часов, убедиться что клиенты работают корректно
|
||||
7. Удалить standalone namespace и PVC
|
||||
```
|
||||
|
||||
### Расширение объёма без миграции (только standalone)
|
||||
|
||||
Longhorn поддерживает расширение PVC онлайн — без остановки MinIO:
|
||||
|
||||
```bash
|
||||
kubectl patch pvc minio -n minio \
|
||||
-p '{"spec":{"resources":{"requests":{"storage":"2Ti"}}}}'
|
||||
|
||||
# Longhorn расширит том; MinIO увидит новое пространство автоматически
|
||||
mc admin info prod # проверить новый объём в разделе capacity
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Итоги и рекомендации
|
||||
|
||||
### Почему Longhorn — правильный выбор для этого кластера
|
||||
|
||||
Longhorn уже является стандартом хранилища в кластере. Использование его для MinIO даёт:
|
||||
|
||||
- **Единая точка управления** — диски, снэпшоты, репликация управляются через Longhorn UI, без отдельной операционной нагрузки
|
||||
- **Онлайн-расширение** — PVC MinIO расширяется без остановки пода (`kubectl patch pvc`)
|
||||
- **Путь к отказоустойчивости** — при добавлении второго worker достаточно поднять `numberOfReplicas: 2`, MinIO продолжает работать без изменений
|
||||
- **Снэпшоты как бэкап** — Longhorn умеет делать снэпшоты PVC по расписанию; для MinIO standalone это основной механизм защиты данных до перехода на distributed
|
||||
|
||||
### Порядок развёртывания
|
||||
|
||||
```
|
||||
1. Применить StorageClass longhorn-minio (см. раздел 3)
|
||||
(опц.) Longhorn UI → k8s-worker-01 → добавить тег "minio" на longhorn-disk1 для изоляции
|
||||
2. kubectl create namespace minio
|
||||
3. Создать Secret minio-root-credentials вручную
|
||||
4. Запустить setup_minio.yml (Helm install)
|
||||
5. Добавить minio-api и minio-console в traefik_port_map (порты 10005, 10006 — свободны)
|
||||
6. Запустить setup_traefik.yml
|
||||
7. Проверить Console: http://10.203.0.96:10006
|
||||
8. Создать пользователей через mc (loki, backup)
|
||||
9. Обновить loki-values.yml если Loki уже работает
|
||||
```
|
||||
|
||||
### Итоговые адреса
|
||||
|
||||
| Сервис | URL |
|
||||
|---|---|
|
||||
| MinIO Console (UI) | `http://10.203.0.96:10006` |
|
||||
| MinIO S3 API | `http://10.203.0.96:10005` |
|
||||
| MinIO API (внутри кластера) | `http://minio.minio.svc.cluster.local:9000` |
|
||||
|
||||
### Следующие шаги
|
||||
|
||||
- [ ] Применить StorageClass `longhorn-minio` (раздел 3)
|
||||
- [ ] (опц.) Longhorn UI: добавить тег `minio` на `longhorn-disk1` для изоляции диска
|
||||
- [ ] Создать Secret `minio-root-credentials` вручную
|
||||
- [ ] Написать роль `roles/minio/` по структуре из раздела 9
|
||||
- [ ] Добавить job `setup/minio` в `.gitlab-ci.yml`
|
||||
- [ ] Добавить `minio-api` и `minio-console` в `traefik_port_map` (порты 10005, 10006)
|
||||
- [ ] Добавить переменные `MINIO_ROOT_PASSWORD`, `MINIO_LOKI_PASSWORD` в GitLab CI/CD Variables
|
||||
- [ ] Добавить firewalld-правило в `roles/minio/tasks/firewall.yml` (порты 9000, 9001 в `trusted` zone для flannel.1/cni0)
|
||||
173
research/traefik-forwardauth.md
Normal file
173
research/traefik-forwardauth.md
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
# Traefik ForwardAuth: HTML-логин для внутренних сервисов
|
||||
|
||||
## 1. Контекст
|
||||
|
||||
Текущая схема использует BasicAuth — браузерный нативный диалог, не кастомизируемый.
|
||||
ForwardAuth позволяет заменить его на HTML-страницу входа с сессиями.
|
||||
|
||||
**Что меняется:** вместо `Middleware basicAuth` → `Middleware forwardAuth` + отдельный auth-pod.
|
||||
|
||||
---
|
||||
|
||||
## 2. Как работает ForwardAuth
|
||||
|
||||
```
|
||||
Клиент → Traefik :10002 → ForwardAuth Middleware
|
||||
│
|
||||
▼
|
||||
auth-сервис /auth
|
||||
│
|
||||
┌───────────┴───────────┐
|
||||
│ 200 OK │ 401 → redirect на /login
|
||||
│ (сессия валидна) │ (HTML-страница логина)
|
||||
▼ ▼
|
||||
longhorn-frontend форма → POST /login
|
||||
│
|
||||
проверка пароля
|
||||
│
|
||||
Set-Cookie session
|
||||
│
|
||||
redirect обратно
|
||||
```
|
||||
|
||||
Traefik делает subrequest на `authResponseHeaders` — если auth-сервис вернул 200, пропускает запрос дальше. Если 401/302 — возвращает клиенту ответ auth-сервиса (redirect на страницу логина).
|
||||
|
||||
---
|
||||
|
||||
## 3. Варианты реализации
|
||||
|
||||
### 3.1. oauth2-proxy (рекомендуется)
|
||||
|
||||
**Что это:** легковесный reverse-proxy с поддержкой htpasswd, GitHub, Google, OIDC.
|
||||
Для внутреннего использования подходит режим `--htpasswd-file` без внешнего провайдера.
|
||||
|
||||
**Плюсы:**
|
||||
- Один pod, ~50MB RAM
|
||||
- Htpasswd-файл через Kubernetes Secret — никаких внешних зависимостей
|
||||
- HTML-форма из коробки, можно переопределить шаблоны
|
||||
- Cookie-сессии, configurable TTL
|
||||
|
||||
**Минусы:**
|
||||
- Один экземпляр oauth2-proxy на весь Traefik (или по одному на сервис)
|
||||
- Нет 2FA
|
||||
|
||||
**Схема деплоя:**
|
||||
```
|
||||
roles/oauth2-proxy/
|
||||
tasks/main.yml — Kubernetes Deployment + Service
|
||||
tasks/secret.yml — инструкция по созданию htpasswd Secret вручную
|
||||
tasks/middleware.yml — ForwardAuth Middleware CRD
|
||||
|
||||
inventory/prod/group_vars/oauth2proxy.yml — cookie_secret, upstream, htpasswd ref
|
||||
```
|
||||
|
||||
**Ключевые параметры:**
|
||||
```yaml
|
||||
# Helm values / env vars
|
||||
--provider=htpasswd
|
||||
--htpasswd-file=/etc/oauth2-proxy/htpasswd
|
||||
--cookie-secret=<32-byte-random> # генерируется вручную
|
||||
--cookie-secure=false # HTTP, без TLS
|
||||
--email-domain=*
|
||||
--upstream=static://200 # ForwardAuth: upstream не используется
|
||||
```
|
||||
|
||||
**Middleware CRD:**
|
||||
```yaml
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: forwardauth-oauth2proxy
|
||||
namespace: traefik
|
||||
spec:
|
||||
forwardAuth:
|
||||
address: http://oauth2-proxy.traefik.svc:4180/oauth2/auth
|
||||
trustForwardHeader: true
|
||||
authResponseHeaders:
|
||||
- X-Auth-Request-User
|
||||
- X-Auth-Request-Email
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2. Authelia
|
||||
|
||||
**Что это:** полноценный self-hosted identity provider. HTML-логин, 2FA (TOTP, WebAuthn), LDAP, правила доступа по пользователям/группам.
|
||||
|
||||
**Плюсы:**
|
||||
- Красивая HTML-форма с брендингом
|
||||
- 2FA из коробки
|
||||
- Гранулярный контроль: разные пользователи для разных сервисов
|
||||
|
||||
**Минусы:**
|
||||
- Требует Redis (сессии) + хранилище пользователей (файл или LDAP)
|
||||
- ~150MB RAM минимум
|
||||
- Сложнее в настройке (YAML-конфиг, несколько секретов)
|
||||
|
||||
**Оправдано если:** нужен 2FA или несколько пользователей с разными правами.
|
||||
|
||||
---
|
||||
|
||||
### 3.3. thomseddon/traefik-forward-auth
|
||||
|
||||
**Что это:** минималистичный ForwardAuth-сервис только для Google/OIDC OAuth.
|
||||
Для htpasswd **не подходит** — требует внешний провайдер.
|
||||
|
||||
---
|
||||
|
||||
## 4. Рекомендация
|
||||
|
||||
Для текущего стека (внутренняя сеть, один оператор, нет SSO) — **oauth2-proxy с htpasswd**.
|
||||
|
||||
| Критерий | oauth2-proxy | Authelia |
|
||||
|---|---|---|
|
||||
| RAM | ~50MB | ~150MB |
|
||||
| Внешние зависимости | нет | Redis |
|
||||
| 2FA | нет | да |
|
||||
| Сложность настройки | низкая | средняя |
|
||||
| HTML-логин | да (кастомизируемый) | да (красивый) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Интеграция с текущим port_map
|
||||
|
||||
Схема остаётся прежней — меняется только тип Middleware:
|
||||
|
||||
```yaml
|
||||
# inventory/prod/group_vars/traefik.yml
|
||||
traefik_port_map:
|
||||
- name: longhorn-ui
|
||||
port: 10002
|
||||
backend:
|
||||
namespace: longhorn-system
|
||||
service: longhorn-frontend
|
||||
port: 80
|
||||
scheme: http
|
||||
auth:
|
||||
type: forwardauth # вместо basicauth
|
||||
middleware: forwardauth-oauth2proxy # имя Middleware
|
||||
```
|
||||
|
||||
При `type: forwardauth` шаблон `ingressroute.yml.j2` подставляет ForwardAuth Middleware вместо BasicAuth.
|
||||
При `type: basicauth` — как сейчас.
|
||||
|
||||
---
|
||||
|
||||
## 6. Порядок развёртывания (если решим внедрить)
|
||||
|
||||
1. Добавить роль `oauth2-proxy` в `roles/`
|
||||
2. Добавить `playbooks/setup_oauth2proxy.yml`
|
||||
3. Создать Secret с htpasswd вручную:
|
||||
```bash
|
||||
kubectl create secret generic oauth2proxy-htpasswd \
|
||||
--from-literal=htpasswd="$(htpasswd -nb admin 'pass')" \
|
||||
-n traefik
|
||||
```
|
||||
4. Создать Secret с cookie_secret:
|
||||
```bash
|
||||
kubectl create secret generic oauth2proxy-cookie \
|
||||
--from-literal=cookie-secret="$(openssl rand -base64 32)" \
|
||||
-n traefik
|
||||
```
|
||||
5. Заменить `basicAuth` Middleware на `forwardAuth` в IngressRoute для нужных сервисов
|
||||
6. Удалить старые BasicAuth Middleware и Secret `traefik-auth-longhorn`
|
||||
494
research/traefik.md
Normal file
494
research/traefik.md
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
# Traefik: port-proxy режим для внутренних сервисов
|
||||
|
||||
## 1. Контекст и цель
|
||||
|
||||
**Режим работы:** Traefik как HTTP-прокси на отдельном порту для каждого внутреннего сервиса.
|
||||
|
||||
Вместо маршрутизации по доменному имени каждый сервис получает выделенный порт на worker-ноде. Клиент обращается `http://10.203.0.96:<PORT>` и попадает напрямую на нужный сервис.
|
||||
|
||||
**Что не используем:** HTTPS, Let's Encrypt, hostname-роутинг, Ingress.
|
||||
|
||||
**Что используем:** EntryPoint per service, HTTP Router с catch-all правилом, BasicAuth Middleware для сервисов без авторизации.
|
||||
|
||||
---
|
||||
|
||||
## 2. Концепция Port Map
|
||||
|
||||
Port Map — единственный файл, описывающий всю топологию проксирования. Из него генерируется вся конфигурация Traefik: entrypoints, routers, services, middleware CRD.
|
||||
|
||||
**Расположение:** `inventory/prod/group_vars/traefik.yml`
|
||||
|
||||
Это inventory-файл, а не часть роли — привязка порта к сервису это инфраструктурное решение, а не логика установки.
|
||||
|
||||
### Формат файла
|
||||
|
||||
```yaml
|
||||
# inventory/prod/group_vars/traefik.yml
|
||||
---
|
||||
|
||||
traefik_port_map:
|
||||
|
||||
- name: kubernetes-dashboard
|
||||
description: "Kubernetes Dashboard"
|
||||
port: 10001
|
||||
backend:
|
||||
namespace: kubernetes-dashboard
|
||||
service: kubernetes-dashboard
|
||||
port: 443 # порт Service; K8s перенаправит на pod:9090 (insecure HTTP)
|
||||
scheme: http # dashboard работает без TLS (--insecure-port=9090 --port=0)
|
||||
basicauth:
|
||||
enabled: false # Dashboard имеет собственную авторизацию по токену — BasicAuth не нужен
|
||||
|
||||
- name: longhorn-ui
|
||||
description: "Longhorn Storage UI"
|
||||
port: 10002
|
||||
backend:
|
||||
namespace: longhorn-system
|
||||
service: longhorn-frontend
|
||||
port: 80
|
||||
scheme: http
|
||||
basicauth:
|
||||
enabled: true
|
||||
secret_name: traefik-auth-longhorn # Longhorn UI без авторизации — защищаем BasicAuth
|
||||
|
||||
# Шаблон для будущих сервисов:
|
||||
# - name: my-service
|
||||
# description: "Human-readable описание"
|
||||
# port: 10003 # уникальный порт в диапазоне 10000-10999
|
||||
# backend:
|
||||
# namespace: my-namespace
|
||||
# service: my-service-name
|
||||
# port: 8080
|
||||
# scheme: http
|
||||
# basicauth:
|
||||
# enabled: false # если сервис имеет собственную авторизацию
|
||||
```
|
||||
|
||||
### Поля Port Map
|
||||
|
||||
| Поле | Обязательное | Описание |
|
||||
|---|---|---|
|
||||
| `name` | да | Уникальный идентификатор (используется в именах K8s-ресурсов) |
|
||||
| `description` | нет | Человекочитаемое описание |
|
||||
| `port` | да | Порт на Traefik-ноде, диапазон `10000–10999` |
|
||||
| `backend.namespace` | да | Kubernetes namespace сервиса |
|
||||
| `backend.service` | да | Имя Kubernetes Service |
|
||||
| `backend.port` | да | Порт Service (не pod-а) |
|
||||
| `backend.scheme` | да | `http` или `https` (протокол, которым Traefik достучится до Service) |
|
||||
| `basicauth.enabled` | нет | `true` — прикрепить BasicAuth; дефолт `false` |
|
||||
| `basicauth.secret_name` | если enabled | Имя Secret в namespace `traefik`; **создаётся вручную** |
|
||||
|
||||
---
|
||||
|
||||
## 3. Управление credentials: только вручную, не через Ansible
|
||||
|
||||
**Принцип разделения ответственности:**
|
||||
|
||||
- **Ansible управляет инфраструктурой:** Middleware CRD, Helm-чарт, firewall, IngressRoute.
|
||||
- **Оператор управляет секретами:** Kubernetes Secret с htpasswd-строками создаётся вручную через Dashboard или kubectl.
|
||||
|
||||
Ansible не знает о содержимом секрета — только о его имени (`secret_name` в port_map). Если Secret не существует, Traefik вернёт 401 для всех запросов к этому сервису (безопасный дефолт).
|
||||
|
||||
### Создание Secret вручную
|
||||
|
||||
**Через kubectl (рекомендуется для первого раза):**
|
||||
|
||||
```bash
|
||||
# 1. Сгенерировать htpasswd-строку (требует htpasswd или openssl)
|
||||
htpasswd -nb admin 'yourpassword'
|
||||
# Вывод: admin:$apr1$xyz...
|
||||
|
||||
# 2. Создать Secret в namespace traefik
|
||||
kubectl create secret generic traefik-auth-dashboard \
|
||||
--from-literal=users='admin:$apr1$xyz...' \
|
||||
-n traefik
|
||||
|
||||
# Для второго сервиса — отдельный Secret:
|
||||
kubectl create secret generic traefik-auth-longhorn \
|
||||
--from-literal=users='admin:$apr1$abc...' \
|
||||
-n traefik
|
||||
```
|
||||
|
||||
**Через Kubernetes Dashboard:**
|
||||
|
||||
1. Открыть Dashboard → Namespace: `traefik` → Secrets → Create
|
||||
2. Тип: `Opaque`
|
||||
3. Имя: `traefik-auth-dashboard` (или как указано в `secret_name`)
|
||||
4. Ключ: `users`
|
||||
5. Значение: htpasswd-строка, например `admin:$apr1$xyz...`
|
||||
|
||||
**Генерация htpasswd без утилиты htpasswd:**
|
||||
|
||||
```bash
|
||||
# Через openssl (доступен везде)
|
||||
openssl passwd -apr1 'yourpassword'
|
||||
|
||||
# Собрать строку вручную:
|
||||
echo "admin:$(openssl passwd -apr1 'yourpassword')"
|
||||
```
|
||||
|
||||
### Обновление пароля
|
||||
|
||||
```bash
|
||||
# Пересоздать Secret с новым паролем
|
||||
kubectl create secret generic traefik-auth-dashboard \
|
||||
--from-literal=users='admin:$apr1$newvalue...' \
|
||||
-n traefik \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
```
|
||||
|
||||
### Разные пользователи для разных сервисов
|
||||
|
||||
Каждый Secret независим. Для одного сервиса можно дать доступ нескольким пользователям — htpasswd-формат поддерживает несколько строк:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic traefik-auth-longhorn \
|
||||
--from-literal=users='admin:$apr1$...'$'\n''devops:$apr1$...' \
|
||||
-n traefply
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Архитектура
|
||||
|
||||
### 4.1. Схема потока
|
||||
|
||||
```
|
||||
Клиент
|
||||
│ http://10.203.0.96:10001 → kubernetes-dashboard (BasicAuth: traefik-auth-dashboard)
|
||||
│ http://10.203.0.96:10002 → longhorn-ui (BasicAuth: traefik-auth-longhorn)
|
||||
│ http://10.203.0.96:10003 → future-service (без BasicAuth)
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ k8s-worker-01 │
|
||||
│ 10.203.0.96 │
|
||||
│ │
|
||||
│ ┌────────────────────────────┐ │
|
||||
│ │ Traefik DaemonSet │ │
|
||||
│ │ │ │
|
||||
│ │ EP :10001 → Middleware ──────► svc kubernetes-dashboard:443 (http→pod:9090)
|
||||
│ │ basicauth-dash │ │
|
||||
│ │ │ │
|
||||
│ │ EP :10002 → Middleware ──────► svc longhorn-frontend:80
|
||||
│ │ basicauth-lhorn │ │
|
||||
│ │ │ │
|
||||
│ │ EP :10003 ───────────────────► svc future:8080 (без auth)
|
||||
│ └────────────────────────────┘ │
|
||||
└────────────────────────────────────┘
|
||||
│ pod network (flannel 10.244.0.0/16)
|
||||
┌───────────┼───────────┐
|
||||
▼ ▼ ▼
|
||||
dashboard longhorn future
|
||||
pod:9090 pod:80 pod:8080
|
||||
```
|
||||
|
||||
### 4.2. Соответствие port_map → Traefik-ресурсы
|
||||
|
||||
Каждая запись в `traefik_port_map` генерирует:
|
||||
|
||||
```
|
||||
"kubernetes-dashboard" (port: 10001, basicauth.enabled: true)
|
||||
│
|
||||
├── EntryPoint "kubernetes-dashboard" port 10001, hostPort 10001
|
||||
├── Middleware "basicauth-kubernetes-dashboard"
|
||||
│ └── basicAuth.secret: traefik-auth-dashboard (Secret создан вручную)
|
||||
├── HTTP Router "router-kubernetes-dashboard"
|
||||
│ entryPoints: [kubernetes-dashboard]
|
||||
│ rule: PathPrefix(`/`)
|
||||
│ middlewares: [basicauth-kubernetes-dashboard]
|
||||
└── HTTP Service "svc-kubernetes-dashboard"
|
||||
url: http://kubernetes-dashboard.kubernetes-dashboard.svc:443
|
||||
```
|
||||
|
||||
### 4.3. Почему Dashboard: scheme: http, port: 443
|
||||
|
||||
В текущей конфигурации Dashboard пропатчен с `--insecure-port=9090 --port=0`:
|
||||
|
||||
```
|
||||
Service spec: port 443 → targetPort 9090
|
||||
Pod: слушает на :9090 по HTTP (без TLS)
|
||||
```
|
||||
|
||||
Traefik подключается к `ClusterIP:443` по протоколу HTTP. Kubernetes направляет трафик на `pod:9090`. TLS нет на всём пути.
|
||||
|
||||
---
|
||||
|
||||
## 5. Структура роли
|
||||
|
||||
```
|
||||
roles/traefik/
|
||||
defaults/main.yml — chart version, namespace, kubeconfig path
|
||||
tasks/main.yml — include_tasks по шагам
|
||||
tasks/helm.yml — helm upgrade --install
|
||||
tasks/middleware.yml — Middleware CRD (один на сервис, если basicauth.enabled)
|
||||
tasks/routes.yml — IngressRoute CRD (один на сервис)
|
||||
tasks/firewall.yml — открыть диапазон 10000-10999 на workers (delegate_to)
|
||||
templates/
|
||||
traefik-values.yml.j2 — Helm values, генерируется из traefik_port_map
|
||||
basicauth-middleware.yml.j2 — Middleware CRD (один экземпляр, рендерится в цикле)
|
||||
ingressroute.yml.j2 — IngressRoute CRD (один экземпляр, рендерится в цикле)
|
||||
|
||||
playbooks/
|
||||
setup_traefik.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Ключевые конфигурационные артефакты
|
||||
|
||||
### 6.1. `roles/traefik/defaults/main.yml`
|
||||
|
||||
```yaml
|
||||
---
|
||||
traefik_chart_version: "32.1.0"
|
||||
traefik_namespace: traefik
|
||||
traefik_kubeconfig: "/home/{{ ansible_user }}/.kube/config"
|
||||
traefik_port_map: [] # переопределяется в inventory/prod/group_vars/traefik.yml
|
||||
```
|
||||
|
||||
### 6.2. `roles/traefik/templates/traefik-values.yml.j2`
|
||||
|
||||
```yaml
|
||||
deployment:
|
||||
kind: DaemonSet
|
||||
|
||||
nodeSelector:
|
||||
kubernetes.io/os: linux
|
||||
|
||||
ports:
|
||||
{% for svc in traefik_port_map %}
|
||||
{{ svc.name }}:
|
||||
port: {{ svc.port }}
|
||||
hostPort: {{ svc.port }}
|
||||
expose:
|
||||
default: true
|
||||
exposedPort: {{ svc.port }}
|
||||
protocol: TCP
|
||||
{% endfor %}
|
||||
web:
|
||||
expose:
|
||||
default: false
|
||||
websecure:
|
||||
expose:
|
||||
default: false
|
||||
|
||||
ingressRoute:
|
||||
dashboard:
|
||||
enabled: false
|
||||
|
||||
providers:
|
||||
kubernetesCRD:
|
||||
enabled: true
|
||||
kubernetesIngress:
|
||||
enabled: false
|
||||
|
||||
persistence:
|
||||
enabled: false
|
||||
|
||||
logs:
|
||||
general:
|
||||
level: INFO
|
||||
access:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
### 6.3. `roles/traefik/templates/basicauth-middleware.yml.j2`
|
||||
|
||||
Рендерится в цикле для каждого сервиса с `basicauth.enabled: true`:
|
||||
|
||||
```yaml
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: basicauth-{{ item.name }}
|
||||
namespace: {{ traefik_namespace }}
|
||||
spec:
|
||||
basicAuth:
|
||||
secret: {{ item.basicauth.secret_name }}
|
||||
removeHeader: true
|
||||
```
|
||||
|
||||
### 6.4. `roles/traefik/templates/ingressroute.yml.j2`
|
||||
|
||||
```yaml
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: route-{{ item.name }}
|
||||
namespace: {{ traefik_namespace }}
|
||||
spec:
|
||||
entryPoints:
|
||||
- {{ item.name }}
|
||||
routes:
|
||||
- match: PathPrefix(`/`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: {{ item.backend.service }}
|
||||
namespace: {{ item.backend.namespace }}
|
||||
port: {{ item.backend.port }}
|
||||
scheme: {{ item.backend.scheme }}
|
||||
{% if item.basicauth.enabled | default(false) %}
|
||||
middlewares:
|
||||
- name: basicauth-{{ item.name }}
|
||||
namespace: {{ traefik_namespace }}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### 6.5. `roles/traefik/tasks/middleware.yml`
|
||||
|
||||
```yaml
|
||||
---
|
||||
- name: Middleware | Render BasicAuth CRD manifests
|
||||
ansible.builtin.template:
|
||||
src: basicauth-middleware.yml.j2
|
||||
dest: "/tmp/traefik-middleware-{{ item.name }}.yml"
|
||||
mode: "0600"
|
||||
loop: "{{ traefik_port_map | selectattr('basicauth.enabled', 'defined') | selectattr('basicauth.enabled') | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
|
||||
- name: Middleware | Apply BasicAuth CRD manifests
|
||||
ansible.builtin.command: >
|
||||
kubectl apply -f /tmp/traefik-middleware-{{ item.name }}.yml
|
||||
environment:
|
||||
KUBECONFIG: "{{ traefik_kubeconfig }}"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
loop: "{{ traefik_port_map | selectattr('basicauth.enabled', 'defined') | selectattr('basicauth.enabled') | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
register: _mw_apply
|
||||
changed_when: "'configured' in _mw_apply.stdout or 'created' in _mw_apply.stdout"
|
||||
```
|
||||
|
||||
### 6.6. `roles/traefik/tasks/firewall.yml`
|
||||
|
||||
```yaml
|
||||
---
|
||||
- name: Firewall | Open Traefik service port range on workers
|
||||
ansible.posix.firewalld:
|
||||
port: "10000-10999/tcp"
|
||||
permanent: true
|
||||
state: enabled
|
||||
immediate: true
|
||||
delegate_to: "{{ item }}"
|
||||
loop: "{{ groups['workers'] }}"
|
||||
|
||||
- name: Firewall | Trust CNI interfaces on workers
|
||||
ansible.posix.firewalld:
|
||||
zone: trusted
|
||||
interface: "{{ iface }}"
|
||||
permanent: true
|
||||
state: enabled
|
||||
immediate: true
|
||||
loop: "{{ groups['workers'] }}"
|
||||
loop_control:
|
||||
loop_var: worker_host
|
||||
vars:
|
||||
_cni_ifaces:
|
||||
- flannel.1
|
||||
- cni0
|
||||
delegate_to: "{{ worker_host }}"
|
||||
with_nested:
|
||||
- "{{ groups['workers'] }}"
|
||||
- [flannel.1, cni0]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Сводная таблица портов
|
||||
|
||||
Поддерживать синхронизированной с `inventory/prod/group_vars/traefik.yml`:
|
||||
|
||||
| Порт | Сервис | BasicAuth | Secret | Описание |
|
||||
|---|---|---|---|---|
|
||||
| 10001 | kubernetes-dashboard | нет | — | K8s Dashboard (своя авторизация по токену) |
|
||||
| 10002 | longhorn-frontend | да | `traefik-auth-longhorn` | Longhorn UI |
|
||||
| 10003–10999 | (резерв) | — | — | Будущие сервисы |
|
||||
|
||||
---
|
||||
|
||||
## 8. Порядок развёртывания
|
||||
|
||||
### Шаг 1: Развернуть Traefik через Ansible
|
||||
|
||||
```bash
|
||||
ansible-playbook -i inventory/prod playbooks/setup_traefik.yml
|
||||
```
|
||||
|
||||
Ansible создаёт: DaemonSet, Middleware CRD (ссылается на несуществующие пока Secret), IngressRoute, firewall-правила.
|
||||
|
||||
На данном этапе Traefik вернёт **500** для сервисов с BasicAuth — Secret ещё не создан.
|
||||
|
||||
### Шаг 2: Создать Secrets вручную (только для сервисов с basicauth.enabled: true)
|
||||
|
||||
```bash
|
||||
# Longhorn — BasicAuth включён
|
||||
kubectl create secret generic traefik-auth-longhorn \
|
||||
--from-literal=users="$(htpasswd -nb admin 'yourpassword')" \
|
||||
-n traefik
|
||||
|
||||
# Dashboard — Secret не нужен, сервис защищён собственной авторизацией по токену
|
||||
```
|
||||
|
||||
После создания Secret Traefik подхватит его автоматически (без рестарта).
|
||||
|
||||
### Шаг 3: Проверить доступ
|
||||
|
||||
```bash
|
||||
# Dashboard — открывается без BasicAuth, но потребует токен внутри UI
|
||||
curl http://10.203.0.96:10001/
|
||||
|
||||
# Longhorn — потребует BasicAuth
|
||||
curl -u admin:yourpassword http://10.203.0.96:10002/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Добавление нового сервиса (runbook)
|
||||
|
||||
1. Добавить запись в `inventory/prod/group_vars/traefik.yml`
|
||||
2. Перезапустить `setup_traefik.yml`
|
||||
3. Создать Secret, если `basicauth.enabled: true`:
|
||||
```bash
|
||||
kubectl create secret generic traefik-auth-<name> \
|
||||
--from-literal=users="$(htpasswd -nb user 'pass')" \
|
||||
-n traefik
|
||||
```
|
||||
4. Обновить таблицу портов в разделе 7 этого документа
|
||||
|
||||
---
|
||||
|
||||
## 10. Интеграция с CI/CD
|
||||
|
||||
```yaml
|
||||
# .gitlab-ci.yml
|
||||
|
||||
# Добавить в syntax-check:
|
||||
- ansible-playbook --syntax-check -i $INVENTORY playbooks/setup_traefik.yml
|
||||
|
||||
# Новый job:
|
||||
setup:traefik:
|
||||
stage: setup
|
||||
script:
|
||||
- ansible-playbook -i $INVENTORY playbooks/setup_traefik.yml
|
||||
environment:
|
||||
name: production
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
when: manual
|
||||
resource_group: production
|
||||
```
|
||||
|
||||
Secrets с паролями **не передаются** через GitLab CI/CD variables — они создаются вручную напрямую в кластере.
|
||||
|
||||
---
|
||||
|
||||
## 11. Риски и gotchas
|
||||
|
||||
| Риск | Митигация |
|
||||
|---|---|
|
||||
| Secret не создан — Traefik возвращает 500 | Ожидаемое поведение; создать Secret и Traefik подхватит без рестарта |
|
||||
| `flannel.1`/`cni0` не в trusted zone | Явная задача в `tasks/firewall.yml` |
|
||||
| hostPort занят | Диапазон 10000–10999 специфический; проверить `ss -tlnp` перед деплоем |
|
||||
| DaemonSet пересоздаётся при добавлении нового порта в Helm values | Краткий downtime ~10с; все существующие соединения рвутся |
|
||||
| Dashboard Service имеет `port: 443` но трафик HTTP | `scheme: http` обязателен; иначе Traefik попытается установить TLS и получит ошибку рукопожатия |
|
||||
1501
roles/flux/README.md
Normal file
1501
roles/flux/README.md
Normal file
File diff suppressed because it is too large
Load diff
19
roles/flux/defaults/main.yml
Normal file
19
roles/flux/defaults/main.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
# Flux CLI version (empty = latest stable from GitHub)
|
||||
flux_version: ""
|
||||
|
||||
# Path to kubeconfig on manager node
|
||||
flux_kubeconfig: "/home/{{ ansible_user }}/.kube/config"
|
||||
|
||||
# GitLab connection settings for bootstrap
|
||||
flux_gitlab_hostname: "gitlab.gigacoms.info"
|
||||
flux_gitlab_owner: "k8s"
|
||||
flux_gitlab_repository: "k8s-fleet"
|
||||
flux_gitlab_branch: "main"
|
||||
flux_gitlab_path: "clusters/production"
|
||||
|
||||
# Whether the owner is a personal account (true) or a group (false)
|
||||
flux_gitlab_personal: false
|
||||
|
||||
# GitLab token passed via env in CI — never hardcode here
|
||||
flux_gitlab_token: "{{ lookup('env', 'GITLAB_FLUX_TOKEN') | default('') }}"
|
||||
1
roles/flux/handlers/main.yml
Normal file
1
roles/flux/handlers/main.yml
Normal file
|
|
@ -0,0 +1 @@
|
|||
---
|
||||
11
roles/flux/meta/main.yml
Normal file
11
roles/flux/meta/main.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
galaxy_info:
|
||||
author: ops
|
||||
description: Install Flux CD CLI and bootstrap Flux in Kubernetes cluster via GitLab
|
||||
license: MIT
|
||||
min_ansible_version: "2.14"
|
||||
platforms:
|
||||
- name: EL
|
||||
versions:
|
||||
- "9"
|
||||
dependencies: []
|
||||
25
roles/flux/tasks/bootstrap.yml
Normal file
25
roles/flux/tasks/bootstrap.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
- name: Bootstrap | Verify GITLAB_FLUX_TOKEN is set
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- flux_gitlab_token | length > 0
|
||||
fail_msg: "GITLAB_FLUX_TOKEN environment variable is not set — cannot bootstrap Flux"
|
||||
success_msg: "GitLab token is present"
|
||||
|
||||
- name: Bootstrap | Run flux bootstrap gitlab
|
||||
ansible.builtin.command:
|
||||
argv: >-
|
||||
{{ ['flux', 'bootstrap', 'gitlab',
|
||||
'--hostname=' + flux_gitlab_hostname,
|
||||
'--owner=' + flux_gitlab_owner,
|
||||
'--repository=' + flux_gitlab_repository,
|
||||
'--branch=' + flux_gitlab_branch,
|
||||
'--path=' + flux_gitlab_path,
|
||||
'--private=true']
|
||||
+ (['--personal'] if flux_gitlab_personal | bool else []) }}
|
||||
environment:
|
||||
KUBECONFIG: "{{ flux_kubeconfig }}"
|
||||
GITLAB_TOKEN: "{{ flux_gitlab_token }}"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
register: _flux_bootstrap
|
||||
changed_when: "'already exists' not in _flux_bootstrap.stderr"
|
||||
48
roles/flux/tasks/install.yml
Normal file
48
roles/flux/tasks/install.yml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
- name: Flux CLI | Get latest version from GitHub
|
||||
ansible.builtin.uri:
|
||||
url: https://api.github.com/repos/fluxcd/flux2/releases/latest
|
||||
return_content: true
|
||||
register: _flux_latest
|
||||
when: not flux_version
|
||||
check_mode: false
|
||||
|
||||
- name: Flux CLI | Set version fact (latest)
|
||||
ansible.builtin.set_fact:
|
||||
_flux_version: "{{ _flux_latest.json.tag_name | regex_replace('^v', '') }}"
|
||||
when: not flux_version
|
||||
|
||||
- name: Flux CLI | Set version fact (pinned)
|
||||
ansible.builtin.set_fact:
|
||||
_flux_version: "{{ flux_version | regex_replace('^v', '') }}"
|
||||
when: flux_version
|
||||
|
||||
- name: Flux CLI | Download and unpack
|
||||
ansible.builtin.unarchive:
|
||||
src: "https://github.com/fluxcd/flux2/releases/download/v{{ _flux_version }}/flux_{{ _flux_version }}_linux_amd64.tar.gz"
|
||||
dest: /usr/local/bin
|
||||
remote_src: true
|
||||
include:
|
||||
- flux
|
||||
creates: /usr/local/bin/flux
|
||||
|
||||
- name: Flux CLI | Set permissions
|
||||
ansible.builtin.file:
|
||||
path: /usr/local/bin/flux
|
||||
mode: "0755"
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: Flux CLI | Enable bash completion
|
||||
ansible.builtin.shell: /usr/local/bin/flux completion bash > /etc/bash_completion.d/flux
|
||||
args:
|
||||
creates: /etc/bash_completion.d/flux
|
||||
|
||||
- name: Flux CLI | Verify pre-flight checks
|
||||
ansible.builtin.command: flux check --pre
|
||||
environment:
|
||||
KUBECONFIG: "{{ flux_kubeconfig }}"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
register: _flux_check
|
||||
changed_when: false
|
||||
failed_when: _flux_check.rc != 0
|
||||
6
roles/flux/tasks/main.yml
Normal file
6
roles/flux/tasks/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: Flux | Install CLI
|
||||
ansible.builtin.include_tasks: install.yml
|
||||
|
||||
- name: Flux | Bootstrap with GitLab
|
||||
ansible.builtin.include_tasks: bootstrap.yml
|
||||
31
roles/gitlab_agent/defaults/main.yml
Normal file
31
roles/gitlab_agent/defaults/main.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
# GitLab hostname
|
||||
gitlab_agent_hostname: "gitlab.gigacoms.info"
|
||||
|
||||
# Agent name — must match the name registered in GitLab UI
|
||||
# and the directory name in .gitlab/agents/<name>/config.yaml in the fleet repo
|
||||
gitlab_agent_name: "production"
|
||||
|
||||
# Kubernetes namespace for agentk
|
||||
gitlab_agent_namespace: "gitlab-agent"
|
||||
|
||||
# Agent token — passed via GITLAB_AGENT_TOKEN CI variable
|
||||
gitlab_agent_token: "{{ lookup('env', 'GITLAB_AGENT_TOKEN') | default('') }}"
|
||||
|
||||
# KAS WebSocket address (auto-derived from hostname)
|
||||
gitlab_agent_kas_address: "wss://{{ gitlab_agent_hostname }}/-/kubernetes-agent/"
|
||||
|
||||
# Path to kubeconfig on manager node
|
||||
gitlab_agent_kubeconfig: "/home/{{ ansible_user }}/.kube/config"
|
||||
|
||||
# Fleet repo settings (for creating agent config.yaml)
|
||||
gitlab_agent_fleet_repo: "https://{{ gitlab_agent_hostname }}/k8s/k8s-fleet.git"
|
||||
gitlab_agent_fleet_token: "{{ lookup('env', 'GITLAB_FLUX_TOKEN') | default('') }}"
|
||||
gitlab_agent_fleet_branch: "main"
|
||||
|
||||
# GitLab group to grant ci_access (all projects in this group can use the agent)
|
||||
gitlab_agent_ci_access_group: "k8s"
|
||||
|
||||
# Additional individual projects to grant ci_access (outside the main group)
|
||||
# Format: list of GitLab project paths, e.g. ["it-dept/billing-mobile"]
|
||||
gitlab_agent_ci_access_projects: []
|
||||
1
roles/gitlab_agent/handlers/main.yml
Normal file
1
roles/gitlab_agent/handlers/main.yml
Normal file
|
|
@ -0,0 +1 @@
|
|||
---
|
||||
11
roles/gitlab_agent/meta/main.yml
Normal file
11
roles/gitlab_agent/meta/main.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
galaxy_info:
|
||||
author: ops
|
||||
description: Install GitLab Agent (agentk) in Kubernetes cluster and register agent config in fleet repo
|
||||
license: MIT
|
||||
min_ansible_version: "2.14"
|
||||
platforms:
|
||||
- name: EL
|
||||
versions:
|
||||
- "9"
|
||||
dependencies: []
|
||||
87
roles/gitlab_agent/tasks/config.yml
Normal file
87
roles/gitlab_agent/tasks/config.yml
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
- name: Agent config | Verify GITLAB_FLUX_TOKEN is set
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- gitlab_agent_fleet_token | length > 0
|
||||
fail_msg: "GITLAB_FLUX_TOKEN environment variable is not set — cannot push agent config to fleet repo"
|
||||
|
||||
- name: Agent config | Create temp directory for fleet repo clone
|
||||
ansible.builtin.tempfile:
|
||||
state: directory
|
||||
suffix: fleet
|
||||
register: _fleet_tmpdir
|
||||
|
||||
- name: Agent config | Clone fleet repo
|
||||
ansible.builtin.git:
|
||||
repo: "https://oauth2:{{ gitlab_agent_fleet_token }}@{{ gitlab_agent_hostname }}/k8s/k8s-fleet.git"
|
||||
dest: "{{ _fleet_tmpdir.path }}/k8s-fleet"
|
||||
version: "{{ gitlab_agent_fleet_branch }}"
|
||||
depth: 1
|
||||
no_log: true
|
||||
changed_when: true
|
||||
|
||||
- name: Agent config | Create agent config directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ _fleet_tmpdir.path }}/k8s-fleet/.gitlab/agents/{{ gitlab_agent_name }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Agent config | Write agent config.yaml
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ _fleet_tmpdir.path }}/k8s-fleet/.gitlab/agents/{{ gitlab_agent_name }}/config.yaml"
|
||||
mode: "0644"
|
||||
content: |
|
||||
gitops:
|
||||
reconcile_timeout: 3600s
|
||||
|
||||
observability:
|
||||
logging:
|
||||
level: info
|
||||
|
||||
ci_access:
|
||||
groups:
|
||||
- id: {{ gitlab_agent_ci_access_group }}
|
||||
{% if gitlab_agent_ci_access_projects | length > 0 %}
|
||||
projects:
|
||||
{% for project in gitlab_agent_ci_access_projects %}
|
||||
- id: {{ project }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
register: _agent_config
|
||||
|
||||
- name: Agent config | Commit and push if changed
|
||||
when: _agent_config.changed
|
||||
block:
|
||||
- name: Agent config | Configure git user
|
||||
ansible.builtin.git:
|
||||
repo: "{{ _fleet_tmpdir.path }}/k8s-fleet"
|
||||
user_name: "Ansible"
|
||||
user_email: "ansible@{{ gitlab_agent_hostname }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Agent config | Stage agent config
|
||||
ansible.builtin.git:
|
||||
repo: "{{ _fleet_tmpdir.path }}/k8s-fleet"
|
||||
add:
|
||||
- ".gitlab/agents/{{ gitlab_agent_name }}/config.yaml"
|
||||
changed_when: false
|
||||
|
||||
- name: Agent config | Commit
|
||||
ansible.builtin.git:
|
||||
repo: "{{ _fleet_tmpdir.path }}/k8s-fleet"
|
||||
commit:
|
||||
msg: "feat: add GitLab agent config for {{ gitlab_agent_name }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Agent config | Push
|
||||
ansible.builtin.git:
|
||||
repo: "{{ _fleet_tmpdir.path }}/k8s-fleet"
|
||||
push: true
|
||||
branch: "{{ gitlab_agent_fleet_branch }}"
|
||||
no_log: true
|
||||
changed_when: true
|
||||
|
||||
- name: Agent config | Remove temp directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ _fleet_tmpdir.path }}"
|
||||
state: absent
|
||||
36
roles/gitlab_agent/tasks/helm.yml
Normal file
36
roles/gitlab_agent/tasks/helm.yml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
- name: Helm | Verify GITLAB_AGENT_TOKEN is set
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- gitlab_agent_token | length > 0
|
||||
fail_msg: "GITLAB_AGENT_TOKEN environment variable is not set — cannot install agentk"
|
||||
|
||||
- name: Helm | Add GitLab chart repository
|
||||
ansible.builtin.command: helm repo add gitlab https://charts.gitlab.io
|
||||
environment:
|
||||
KUBECONFIG: "{{ gitlab_agent_kubeconfig }}"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
register: _helm_repo_add
|
||||
changed_when: "'already exists' not in _helm_repo_add.stdout"
|
||||
failed_when: _helm_repo_add.rc != 0 and 'already exists' not in _helm_repo_add.stdout
|
||||
|
||||
- name: Helm | Update chart repositories
|
||||
ansible.builtin.command: helm repo update
|
||||
environment:
|
||||
KUBECONFIG: "{{ gitlab_agent_kubeconfig }}"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
changed_when: false
|
||||
|
||||
- name: Helm | Install or upgrade gitlab-agent
|
||||
ansible.builtin.command: >
|
||||
helm upgrade --install gitlab-agent gitlab/gitlab-agent
|
||||
--namespace {{ gitlab_agent_namespace }}
|
||||
--create-namespace
|
||||
--set config.token={{ gitlab_agent_token }}
|
||||
--set config.kasAddress={{ gitlab_agent_kas_address }}
|
||||
environment:
|
||||
KUBECONFIG: "{{ gitlab_agent_kubeconfig }}"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
no_log: true
|
||||
register: _helm_install
|
||||
changed_when: "'STATUS: deployed' in _helm_install.stdout or 'has been upgraded' in _helm_install.stdout"
|
||||
6
roles/gitlab_agent/tasks/main.yml
Normal file
6
roles/gitlab_agent/tasks/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: GitLab Agent | Push agent config to fleet repo
|
||||
ansible.builtin.include_tasks: config.yml
|
||||
|
||||
- name: GitLab Agent | Install agentk via Helm
|
||||
ansible.builtin.include_tasks: helm.yml
|
||||
6
roles/gpu_device_plugin/defaults/main.yml
Normal file
6
roles/gpu_device_plugin/defaults/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
gpu_device_plugin_version: "v0.17.0"
|
||||
gpu_device_plugin_manifest_url: "https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/{{ gpu_device_plugin_version }}/deployments/static/nvidia-device-plugin.yml"
|
||||
|
||||
# Path to kubeconfig on the manager node
|
||||
gpu_kubeconfig: "/home/{{ ansible_user }}/.kube/config"
|
||||
11
roles/gpu_device_plugin/meta/main.yml
Normal file
11
roles/gpu_device_plugin/meta/main.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
galaxy_info:
|
||||
author: ops
|
||||
description: Labels GPU worker nodes and installs the NVIDIA k8s device plugin
|
||||
license: MIT
|
||||
min_ansible_version: "2.14"
|
||||
platforms:
|
||||
- name: EL
|
||||
versions:
|
||||
- "9"
|
||||
dependencies: []
|
||||
22
roles/gpu_device_plugin/tasks/deploy.yml
Normal file
22
roles/gpu_device_plugin/tasks/deploy.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
- name: Deploy | Check kubeconfig exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gpu_kubeconfig }}"
|
||||
register: _kubeconfig_plugin
|
||||
|
||||
- name: Deploy | Skip notice
|
||||
ansible.builtin.debug:
|
||||
msg: "Device plugin install skipped — kubeconfig not found at {{ gpu_kubeconfig }}."
|
||||
when: not _kubeconfig_plugin.stat.exists
|
||||
|
||||
- name: Deploy | Apply NVIDIA device plugin manifest
|
||||
ansible.builtin.command:
|
||||
cmd: "kubectl apply -f {{ gpu_device_plugin_manifest_url }}"
|
||||
environment:
|
||||
KUBECONFIG: "{{ gpu_kubeconfig }}"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
when:
|
||||
- _kubeconfig_plugin.stat.exists
|
||||
- groups['gpu_workers'] | default([]) | length > 0
|
||||
register: _plugin_apply
|
||||
changed_when: "'configured' in _plugin_apply.stdout or 'created' in _plugin_apply.stdout"
|
||||
19
roles/gpu_device_plugin/tasks/label.yml
Normal file
19
roles/gpu_device_plugin/tasks/label.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
- name: Label | Check kubeconfig exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gpu_kubeconfig }}"
|
||||
register: _kubeconfig_gpu
|
||||
|
||||
- name: Label | Apply node_labels to each GPU worker
|
||||
ansible.builtin.command:
|
||||
argv: "{{ ['kubectl', 'label', 'node', item, '--overwrite'] + (hostvars[item].node_labels.items() | map('join', '=') | list) }}"
|
||||
environment:
|
||||
KUBECONFIG: "{{ gpu_kubeconfig }}"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
loop: "{{ groups['gpu_workers'] | default([]) }}"
|
||||
when:
|
||||
- _kubeconfig_gpu.stat.exists
|
||||
- hostvars[item].node_labels | default({}) | length > 0
|
||||
register: _label
|
||||
changed_when: "'labeled' in _label.stdout"
|
||||
failed_when: _label.rc != 0
|
||||
6
roles/gpu_device_plugin/tasks/main.yml
Normal file
6
roles/gpu_device_plugin/tasks/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: Label nodes
|
||||
ansible.builtin.include_tasks: label.yml
|
||||
|
||||
- name: Install device plugin
|
||||
ansible.builtin.include_tasks: deploy.yml
|
||||
129
roles/gpu_model_storage/README.md
Normal file
129
roles/gpu_model_storage/README.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# gpu_model_storage
|
||||
|
||||
Выделенный локальный RAID1-раздел под большие файлы моделей (Ollama и т.п.) на GPU-узлах кластера.
|
||||
|
||||
## Зачем это нужно
|
||||
|
||||
`gpu_workers` намеренно исключены из Longhorn (`longhorn_disks: []` в `group_vars/gpu_workers.yml`) —
|
||||
GPU-узлы являются compute-only, а не storage-нодами.
|
||||
|
||||
При этом проект `k8s_ai` (Ollama + Ollama Proxy + Open WebUI) хранит файлы моделей через статический
|
||||
`hostPath` PV (`k8s_ai/k8s/ollama/pvc-models.yaml`), привязанный к конкретному узлу через `nodeAffinity`.
|
||||
Изначально этот `hostPath` указывал на `/root/ollama-models`, что физически находится на **корневой
|
||||
файловой системе** узла.
|
||||
|
||||
Корневой раздел на `k8s-worker-02` — это RAID1-массив `md126` размером всего **~30 ГБ**
|
||||
(`sda4`/`sdb4`). Первая же загрузка модели среднего размера (обычно десятки гигабайт) заполнила бы
|
||||
диск полностью и уронила бы узел — на `/` живут `systemd`, `containerd`, `kubelet` и вся остальная ОС.
|
||||
Kubernetes при этом **не** проверяет заявленную ёмкость `hostPath` PV (`capacity.storage` — чисто
|
||||
декларативное поле), так что переполнение диска не было бы предотвращено на уровне Kubernetes.
|
||||
|
||||
При этом два зеркальных диска узла (`sda`/`sdb`, по 4 ТБ каждый) используют под ОС (`boot`,
|
||||
`boot_efi`, `root`) только первые ~34 ГБ — оставшиеся ~3.6 ТБ на каждом диске были полностью
|
||||
неразмеченными и простаивали.
|
||||
|
||||
**Решение**: роль нарезает из этого свободного места отдельный RAID1-раздел, форматирует его и
|
||||
монтирует в отдельную точку — модели больше не могут повлиять на работоспособность ОС.
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
sda, sdb (по 4 ТБ, зеркало)
|
||||
├─ sda1/sdb1 → md127 (boot, ext4, ~1 ГБ) — существовало до роли
|
||||
├─ sda2/sdb2 → md125 (boot_efi, vfat, ~0.6 ГБ) — существовало до роли
|
||||
├─ sda3/sdb3 → LVM (swap) — существовало до роли
|
||||
├─ sda4/sdb4 → md126 (root, ext4, ~30 ГБ) — существовало до роли
|
||||
└─ sda5/sdb5 → md1 (модели, xfs, gpu_model_storage_partition_size_gb ГБ) ← создаёт эта роль
|
||||
└─ смонтирован в gpu_model_storage_mountpoint (по умолчанию /mnt/ollama-models)
|
||||
```
|
||||
|
||||
Партиция №5 создаётся сразу после существующих ОС-партиций (старт фиксирован на `34GB`, конец —
|
||||
`34 + gpu_model_storage_partition_size_gb` ГБ), на каждом диске из `gpu_model_storage_devices`.
|
||||
Из этих партиций собирается новый независимый `mdadm`-массив RAID1 (не имеет отношения к
|
||||
`md126`/`md127`/`md125` — отдельный массив, отдельная точка монтирования).
|
||||
|
||||
## Как включить для узла/группы
|
||||
|
||||
Роль **выключена по умолчанию** (`gpu_model_storage_devices: []` в `defaults/main.yml`) — по тому
|
||||
же паттерну, что и `longhorn_disks` в `longhorn_prereqs`. Чтобы включить, переопределите список
|
||||
дисков в `group_vars/<group>.yml` или `hosts.yml` для конкретного хоста:
|
||||
|
||||
```yaml
|
||||
# inventory/prod/group_vars/gpu_workers.yml
|
||||
gpu_model_storage_devices:
|
||||
- /dev/sda
|
||||
- /dev/sdb
|
||||
gpu_model_storage_partition_size_gb: 1000
|
||||
```
|
||||
|
||||
Сейчас это включено для всей группы `gpu_workers` (на данный момент единственный член —
|
||||
`k8s-worker-02`).
|
||||
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
ansible-playbook -i inventory/prod playbooks/setup_gpu_model_storage.yml
|
||||
```
|
||||
|
||||
Плейбук нацелен на группу `gpu_workers`. Порядок относительно `setup_worker_plane.yml` /
|
||||
`setup_gpu.yml` **не важен** — роль работает только с локальными дисками узла и не трогает
|
||||
`containerd`/`kubelet`/сеть. Можно запускать в любой момент, включая до `kubeadm join`.
|
||||
|
||||
Все задачи идемпотентны:
|
||||
- `community.general.parted` не пересоздаёт партицию, если она уже существует с нужными параметрами.
|
||||
- `raid.yml` проверяет `mdadm --detail <device>` перед созданием массива — повторный запуск не
|
||||
пересоздаёт RAID.
|
||||
- `filesystem.yml` (`community.general.filesystem`) не переформатирует уже отформатированный раздел.
|
||||
- `mount.yml` использует `ansible.posix.mount` с `state: mounted` — идемпотентно монтирует по UUID.
|
||||
|
||||
## Все переменные
|
||||
|
||||
| Переменная | Значение по умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `gpu_model_storage_devices` | `[]` | Список блочных устройств — членов будущего RAID1 (например `/dev/sda`, `/dev/sdb`). Пустой список = роль ничего не делает. |
|
||||
| `gpu_model_storage_partition_size_gb` | `1000` | Размер новой партиции (пятой) на каждом устройстве, в гигабайтах. Партиция начинается сразу после существующих ОС-партиций (`34GB`). |
|
||||
| `gpu_model_storage_raid_device` | `/dev/md1` | Имя нового RAID1-массива. Не должно совпадать с уже существующими (`/dev/md125`, `/dev/md126`, `/dev/md127` на типовом узле). |
|
||||
| `gpu_model_storage_mountpoint` | `/mnt/ollama-models` | Точка монтирования нового раздела. |
|
||||
| `gpu_model_storage_fstype` | `xfs` | Файловая система (как в Longhorn-дисках — консистентность в репозитории). |
|
||||
|
||||
## Ограничения и подводные камни
|
||||
|
||||
- **Начало партиции захардкожено в `34GB`.** Это соответствует текущему размеру существующих
|
||||
ОС-партиций (`boot` + `boot_efi` + `swap` + `root` ≈ 32.2 ГБ + небольшой запас) на узлах,
|
||||
разворачиваемых по стандартному Rocky Linux 9 + RAID1 layout. Если разметка ОС на новом узле
|
||||
отличается — проверьте фактическое свободное место (`parted /dev/sdX unit GB print free`) и
|
||||
скорректируйте `part_start` в `roles/gpu_model_storage/tasks/partition.yml` перед запуском.
|
||||
- **Имя RAID-устройства должно быть свободным.** Перед первым запуском на новом узле проверьте
|
||||
`cat /proc/mdstat`, чтобы `gpu_model_storage_raid_device` не совпадал с уже занятым `/dev/mdN`.
|
||||
- **Роль не выполняет `dracut -f`** — это осознанно: массив не является частью `root`/`boot`, не
|
||||
участвует в загрузке системы, поэтому пересборка initramfs не требуется. Ядро подхватывает массив
|
||||
через udev по суперблоку при каждой загрузке, а запись в `/etc/mdadm.conf` — для надёжности
|
||||
(явное ARRAY-определение вместо авто-скана).
|
||||
- **Диски используются только на ~1/4** (при `gpu_model_storage_partition_size_gb: 1000` из ~3967 ГБ
|
||||
свободных) — сделано намеренно, с запасом на будущее (другие датасеты, второй RAID-раздел и т.д.).
|
||||
Увеличить размер существующего раздела после создания массива штатными средствами Ansible-роли
|
||||
нельзя — потребуется resize партиции, RAID и файловой системы вручную (`parted resizepart`,
|
||||
`mdadm --grow`, `xfs_growfs`).
|
||||
- **Реальная доступная ёмкость меньше заявленных 1000 ГБ** из-за разницы GB/GiB и служебных данных
|
||||
файловой системы. В `k8s_ai/k8s/ollama/pvc-models.yaml` `hostPath`/`PVC` объявляют `900Gi` —
|
||||
безопасный запас под это расхождение (это не enforced-лимит, просто декларативное число).
|
||||
|
||||
## Диагностика
|
||||
|
||||
```bash
|
||||
# Статус RAID-массива
|
||||
cat /proc/mdstat
|
||||
mdadm --detail /dev/md1
|
||||
|
||||
# Точка монтирования
|
||||
lsblk -f
|
||||
df -h /mnt/ollama-models
|
||||
|
||||
# Если массив не поднялся после перезагрузки — проверить запись в mdadm.conf
|
||||
grep md1 /etc/mdadm.conf
|
||||
```
|
||||
|
||||
Если `/mnt/ollama-models` не смонтирован после перезагрузки — массив собран (`mdadm --detail`
|
||||
показывает `active`), но запись в `/etc/fstab` (созданная `ansible.posix.mount`) использует
|
||||
`nofail`, поэтому загрузка ОС не блокируется — нужно смонтировать вручную (`mount -a`) и
|
||||
разобраться, почему массив не успел подняться до попытки монтирования при следующем прогоне роли.
|
||||
10
roles/gpu_model_storage/defaults/main.yml
Normal file
10
roles/gpu_model_storage/defaults/main.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
# Dedicated local RAID1 partition for large model files (Ollama etc.) on GPU
|
||||
# worker nodes. Carved out of otherwise-unpartitioned space on the same
|
||||
# mirrored disks used for the OS. Empty by default — override per host/group
|
||||
# (see group_vars/gpu_workers.yml) to enable.
|
||||
gpu_model_storage_devices: []
|
||||
gpu_model_storage_partition_size_gb: 1000
|
||||
gpu_model_storage_raid_device: /dev/md1
|
||||
gpu_model_storage_mountpoint: /mnt/ollama-models
|
||||
gpu_model_storage_fstype: xfs
|
||||
11
roles/gpu_model_storage/meta/main.yml
Normal file
11
roles/gpu_model_storage/meta/main.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
galaxy_info:
|
||||
author: ops
|
||||
description: Dedicated local RAID1 partition + filesystem for large model files (Ollama) on GPU worker nodes
|
||||
license: MIT
|
||||
min_ansible_version: "2.14"
|
||||
platforms:
|
||||
- name: EL
|
||||
versions:
|
||||
- "9"
|
||||
dependencies: []
|
||||
5
roles/gpu_model_storage/tasks/filesystem.yml
Normal file
5
roles/gpu_model_storage/tasks/filesystem.yml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
- name: Filesystem | Format RAID array
|
||||
community.general.filesystem:
|
||||
fstype: "{{ gpu_model_storage_fstype }}"
|
||||
dev: "{{ gpu_model_storage_raid_device }}"
|
||||
16
roles/gpu_model_storage/tasks/main.yml
Normal file
16
roles/gpu_model_storage/tasks/main.yml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
- name: Partition
|
||||
ansible.builtin.include_tasks: partition.yml
|
||||
when: gpu_model_storage_devices | length > 0
|
||||
|
||||
- name: RAID
|
||||
ansible.builtin.include_tasks: raid.yml
|
||||
when: gpu_model_storage_devices | length > 0
|
||||
|
||||
- name: Filesystem
|
||||
ansible.builtin.include_tasks: filesystem.yml
|
||||
when: gpu_model_storage_devices | length > 0
|
||||
|
||||
- name: Mount
|
||||
ansible.builtin.include_tasks: mount.yml
|
||||
when: gpu_model_storage_devices | length > 0
|
||||
19
roles/gpu_model_storage/tasks/mount.yml
Normal file
19
roles/gpu_model_storage/tasks/mount.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
- name: Mount | Create mountpoint directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ gpu_model_storage_mountpoint }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Mount | Get UUID of RAID array
|
||||
ansible.builtin.command: "blkid -s UUID -o value {{ gpu_model_storage_raid_device }}"
|
||||
register: _md_uuid
|
||||
changed_when: false
|
||||
|
||||
- name: Mount | Mount persistently (UUID, nofail)
|
||||
ansible.posix.mount:
|
||||
path: "{{ gpu_model_storage_mountpoint }}"
|
||||
src: "UUID={{ _md_uuid.stdout }}"
|
||||
fstype: "{{ gpu_model_storage_fstype }}"
|
||||
opts: defaults,nofail
|
||||
state: mounted
|
||||
17
roles/gpu_model_storage/tasks/partition.yml
Normal file
17
roles/gpu_model_storage/tasks/partition.yml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
# The existing OS RAID1 partitions (boot, boot_efi, root) occupy the first
|
||||
# ~34GB of each mirrored disk. Partition 5 uses the free space right after
|
||||
# that, sized by gpu_model_storage_partition_size_gb, on every device listed
|
||||
# in gpu_model_storage_devices — the partitions are then mirrored into a new
|
||||
# mdadm RAID1 array in raid.yml.
|
||||
- name: Partition | Create model storage partition (raid member) on {{ item }}
|
||||
community.general.parted:
|
||||
device: "{{ item }}"
|
||||
number: 5
|
||||
state: present
|
||||
part_start: "34GB"
|
||||
part_end: "{{ 34 + gpu_model_storage_partition_size_gb }}GB"
|
||||
flags: [raid]
|
||||
loop: "{{ gpu_model_storage_devices }}"
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
28
roles/gpu_model_storage/tasks/raid.yml
Normal file
28
roles/gpu_model_storage/tasks/raid.yml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
- name: RAID | Check whether array already exists
|
||||
ansible.builtin.command: "mdadm --detail {{ gpu_model_storage_raid_device }}"
|
||||
register: _md_detail
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: RAID | Create RAID1 array from partition 5 on each device
|
||||
ansible.builtin.command: >
|
||||
mdadm --create {{ gpu_model_storage_raid_device }}
|
||||
--level=1 --raid-devices={{ gpu_model_storage_devices | length }}
|
||||
--metadata=1.2 --run
|
||||
{{ gpu_model_storage_devices | map('regex_replace', '$', '5') | join(' ') }}
|
||||
when: _md_detail.rc != 0
|
||||
|
||||
- name: RAID | Read current mdadm scan output
|
||||
ansible.builtin.command: mdadm --detail --scan
|
||||
register: _mdadm_scan
|
||||
changed_when: false
|
||||
|
||||
- name: RAID | Persist array definition in /etc/mdadm.conf
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/mdadm.conf
|
||||
line: "{{ item }}"
|
||||
regexp: "^ARRAY {{ gpu_model_storage_raid_device }} "
|
||||
loop: "{{ _mdadm_scan.stdout_lines | select('search', gpu_model_storage_raid_device) | list }}"
|
||||
loop_control:
|
||||
label: "{{ gpu_model_storage_raid_device }}"
|
||||
3
roles/gpu_prereqs/defaults/main.yml
Normal file
3
roles/gpu_prereqs/defaults/main.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
nvidia_container_toolkit_version: "" # empty = latest
|
||||
nvidia_container_toolkit_repo_url: "https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo"
|
||||
6
roles/gpu_prereqs/handlers/main.yml
Normal file
6
roles/gpu_prereqs/handlers/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: Restart containerd
|
||||
ansible.builtin.systemd:
|
||||
name: containerd
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
11
roles/gpu_prereqs/meta/main.yml
Normal file
11
roles/gpu_prereqs/meta/main.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
galaxy_info:
|
||||
author: ops
|
||||
description: NVIDIA GPU prerequisites for a Kubernetes worker (Rocky Linux 9)
|
||||
license: MIT
|
||||
min_ansible_version: "2.14"
|
||||
platforms:
|
||||
- name: EL
|
||||
versions:
|
||||
- "9"
|
||||
dependencies: []
|
||||
19
roles/gpu_prereqs/tasks/containerd.yml
Normal file
19
roles/gpu_prereqs/tasks/containerd.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
# Must run after roles/k8s_worker has generated /etc/containerd/config.toml —
|
||||
# k8s_worker regenerates that file from scratch and would wipe this out if it
|
||||
# ran afterwards. Run setup_gpu.yml only after setup_worker_plane.yml.
|
||||
- name: Containerd | Check for nvidia runtime section
|
||||
ansible.builtin.command: grep -q 'runtimes.nvidia' /etc/containerd/config.toml
|
||||
register: _nvidia_runtime_present
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Containerd | Configure nvidia runtime
|
||||
ansible.builtin.command: nvidia-ctk runtime configure --runtime=containerd
|
||||
when: _nvidia_runtime_present.rc != 0
|
||||
notify: Restart containerd
|
||||
|
||||
- name: Containerd | Set nvidia as default runtime
|
||||
ansible.builtin.command: nvidia-ctk runtime configure --runtime=containerd --set-as-default
|
||||
when: _nvidia_runtime_present.rc != 0
|
||||
notify: Restart containerd
|
||||
15
roles/gpu_prereqs/tasks/driver_check.yml
Normal file
15
roles/gpu_prereqs/tasks/driver_check.yml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
# Driver installation is out of scope for this role — Ansible only configures
|
||||
# the container runtime on top of a driver that is already present on the host.
|
||||
- name: Driver check | Look up nvidia-smi
|
||||
ansible.builtin.command: which nvidia-smi
|
||||
register: _nvidia_smi
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Driver check | Fail if NVIDIA driver is not installed
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
nvidia-smi not found on {{ inventory_hostname }}. Install the NVIDIA driver
|
||||
manually before running this role — it is not managed by Ansible.
|
||||
when: _nvidia_smi.rc != 0
|
||||
9
roles/gpu_prereqs/tasks/main.yml
Normal file
9
roles/gpu_prereqs/tasks/main.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
- name: Verify NVIDIA driver
|
||||
ansible.builtin.include_tasks: driver_check.yml
|
||||
|
||||
- name: NVIDIA Container Toolkit
|
||||
ansible.builtin.include_tasks: toolkit.yml
|
||||
|
||||
- name: Containerd GPU runtime
|
||||
ansible.builtin.include_tasks: containerd.yml
|
||||
12
roles/gpu_prereqs/tasks/toolkit.yml
Normal file
12
roles/gpu_prereqs/tasks/toolkit.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
- name: Toolkit | Add NVIDIA Container Toolkit repo
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ nvidia_container_toolkit_repo_url }}"
|
||||
dest: /etc/yum.repos.d/nvidia-container-toolkit.repo
|
||||
mode: "0644"
|
||||
|
||||
- name: Toolkit | Install nvidia-container-toolkit
|
||||
ansible.builtin.dnf:
|
||||
name: "{{ 'nvidia-container-toolkit-' + nvidia_container_toolkit_version if nvidia_container_toolkit_version != '' else 'nvidia-container-toolkit' }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
24
roles/k8s_control_plane/defaults/main.yml
Normal file
24
roles/k8s_control_plane/defaults/main.yml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
# Kubernetes version (major.minor) — used for repo URL and kubeadm
|
||||
k8s_version: "1.33"
|
||||
|
||||
# Container runtime
|
||||
containerd_version: "" # empty = latest from Docker CE repo
|
||||
|
||||
# Network settings
|
||||
pod_network_cidr: "10.244.0.0/16" # Flannel default
|
||||
service_cidr: "10.96.0.0/12" # kubeadm default
|
||||
apiserver_advertise_address: "{{ ansible_host }}"
|
||||
|
||||
# CNI plugin: flannel | calico
|
||||
cni_plugin: "flannel"
|
||||
|
||||
# Fix #6: pinned CNI versions — bump explicitly when upgrading
|
||||
flannel_version: "v0.26.1"
|
||||
calico_version: "v3.27.0"
|
||||
|
||||
# Path where kubeconfig is placed for the ansible_user on the control plane
|
||||
kubeconfig_path: "/home/{{ ansible_user }}/.kube/config"
|
||||
|
||||
# Copy kubeconfig to manager nodes after init
|
||||
kubeconfig_fetch_to_managers: true
|
||||
17
roles/k8s_control_plane/handlers/main.yml
Normal file
17
roles/k8s_control_plane/handlers/main.yml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
- name: restart containerd
|
||||
ansible.builtin.systemd:
|
||||
name: containerd
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
|
||||
- name: restart kubelet
|
||||
ansible.builtin.systemd:
|
||||
name: kubelet
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
|
||||
- name: reload firewalld
|
||||
ansible.builtin.systemd:
|
||||
name: firewalld
|
||||
state: reloaded
|
||||
11
roles/k8s_control_plane/meta/main.yml
Normal file
11
roles/k8s_control_plane/meta/main.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
galaxy_info:
|
||||
author: ops
|
||||
description: Kubernetes control plane node (single master) on Rocky Linux 9
|
||||
license: MIT
|
||||
min_ansible_version: "2.14"
|
||||
platforms:
|
||||
- name: EL
|
||||
versions:
|
||||
- "9"
|
||||
dependencies: []
|
||||
57
roles/k8s_control_plane/tasks/containerd.yml
Normal file
57
roles/k8s_control_plane/tasks/containerd.yml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
- name: Containerd | Add Docker CE repo
|
||||
ansible.builtin.get_url:
|
||||
url: https://download.docker.com/linux/rhel/docker-ce.repo
|
||||
dest: /etc/yum.repos.d/docker-ce.repo
|
||||
mode: "0644"
|
||||
|
||||
- name: Containerd | Disable conflicting container-tools module
|
||||
ansible.builtin.command: dnf module disable container-tools -y
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Containerd | Install containerd.io
|
||||
ansible.builtin.dnf:
|
||||
name: "{{ 'containerd.io-' + containerd_version if containerd_version != '' else 'containerd.io' }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
disablerepo: kubernetes
|
||||
|
||||
- name: Containerd | Ensure runc is available at path expected by containerd
|
||||
ansible.builtin.shell: |
|
||||
RUNC_BIN=$(which runc 2>/dev/null || echo "")
|
||||
if [ -n "$RUNC_BIN" ] && [ ! -f /usr/local/bin/runc ]; then
|
||||
ln -sf "$RUNC_BIN" /usr/local/bin/runc
|
||||
fi
|
||||
changed_when: false
|
||||
|
||||
- name: Containerd | Remove default config
|
||||
ansible.builtin.file:
|
||||
dest: /etc/containerd/config.toml
|
||||
state: absent
|
||||
|
||||
- name: Containerd | Generate default config
|
||||
ansible.builtin.shell: containerd config default > /etc/containerd/config.toml
|
||||
args:
|
||||
creates: /etc/containerd/config.toml
|
||||
|
||||
- name: Containerd | Enable SystemdCgroup
|
||||
ansible.builtin.replace:
|
||||
path: /etc/containerd/config.toml
|
||||
regexp: 'SystemdCgroup = false'
|
||||
replace: 'SystemdCgroup = true'
|
||||
notify: restart containerd
|
||||
|
||||
- name: Containerd | Enable SystemdCgroup
|
||||
ansible.builtin.replace:
|
||||
path: /etc/containerd/config.toml
|
||||
regexp: 'disabled_plugins = ["cri"]'
|
||||
replace: 'disabled_plugins = []'
|
||||
notify: restart containerd
|
||||
|
||||
- name: Containerd | Enable and start service
|
||||
ansible.builtin.systemd:
|
||||
name: containerd
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
133
roles/k8s_control_plane/tasks/kubeadm_init.yml
Normal file
133
roles/k8s_control_plane/tasks/kubeadm_init.yml
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
---
|
||||
- name: kubeadm init | Get full kubeadm version
|
||||
ansible.builtin.command: kubeadm version -o short
|
||||
register: _kubeadm_full_version
|
||||
changed_when: false
|
||||
|
||||
- name: kubeadm init | Check if cluster already initialized
|
||||
ansible.builtin.stat:
|
||||
path: /etc/kubernetes/admin.conf
|
||||
register: _admin_conf
|
||||
|
||||
- name: kubeadm init | Write kubeadm config
|
||||
ansible.builtin.template:
|
||||
src: kubeadm_config.yml.j2
|
||||
dest: /tmp/kubeadm_config.yml
|
||||
mode: "0600"
|
||||
when: not _admin_conf.stat.exists
|
||||
|
||||
- name: kubeadm init | Initialize cluster
|
||||
ansible.builtin.command: >
|
||||
kubeadm init
|
||||
--config /tmp/kubeadm_config.yml
|
||||
--upload-certs
|
||||
when: not _admin_conf.stat.exists
|
||||
register: _kubeadm_init
|
||||
changed_when: true
|
||||
|
||||
- name: kubeadm init | Ensure .kube dir for root
|
||||
ansible.builtin.file:
|
||||
path: /root/.kube
|
||||
state: directory
|
||||
mode: "0700"
|
||||
|
||||
- name: kubeadm init | Copy admin.conf for root
|
||||
ansible.builtin.copy:
|
||||
src: /etc/kubernetes/admin.conf
|
||||
dest: /root/.kube/config
|
||||
remote_src: true
|
||||
mode: "0600"
|
||||
|
||||
- name: kubeadm init | Ensure .kube dir for ansible_user
|
||||
ansible.builtin.file:
|
||||
path: "/home/{{ ansible_user }}/.kube"
|
||||
state: directory
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: "0700"
|
||||
|
||||
- name: kubeadm init | Copy admin.conf for ansible_user
|
||||
ansible.builtin.copy:
|
||||
src: /etc/kubernetes/admin.conf
|
||||
dest: "{{ kubeconfig_path }}"
|
||||
remote_src: true
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: "0600"
|
||||
|
||||
# Fix #6: pinned Flannel version — change flannel_version in defaults to upgrade
|
||||
- name: kubeadm init | Install Flannel CNI
|
||||
ansible.builtin.command: >
|
||||
kubectl apply -f
|
||||
https://github.com/flannel-io/flannel/releases/download/{{ flannel_version }}/kube-flannel.yml
|
||||
environment:
|
||||
KUBECONFIG: /etc/kubernetes/admin.conf
|
||||
when:
|
||||
- cni_plugin == "flannel"
|
||||
- not _admin_conf.stat.exists
|
||||
changed_when: true
|
||||
|
||||
- name: kubeadm init | Install Calico CNI
|
||||
ansible.builtin.command: >
|
||||
kubectl apply -f
|
||||
https://raw.githubusercontent.com/projectcalico/calico/{{ calico_version }}/manifests/calico.yaml
|
||||
environment:
|
||||
KUBECONFIG: /etc/kubernetes/admin.conf
|
||||
when:
|
||||
- cni_plugin == "calico"
|
||||
- not _admin_conf.stat.exists
|
||||
changed_when: true
|
||||
|
||||
# single-node cluster — remove taint so workloads can schedule on master
|
||||
- name: kubeadm init | Remove control-plane taint (single-node)
|
||||
ansible.builtin.command: >
|
||||
kubectl taint nodes {{ inventory_hostname }}
|
||||
node-role.kubernetes.io/control-plane:NoSchedule-
|
||||
environment:
|
||||
KUBECONFIG: /etc/kubernetes/admin.conf
|
||||
when: not _admin_conf.stat.exists
|
||||
failed_when: false
|
||||
changed_when: true
|
||||
|
||||
# Fix #5: save join command so worker nodes can be added later (tokens expire in 24h)
|
||||
- name: kubeadm init | Save worker join command
|
||||
ansible.builtin.shell: >
|
||||
kubeadm token create --print-join-command
|
||||
register: _join_command
|
||||
changed_when: false
|
||||
|
||||
- name: kubeadm init | Write join command to file
|
||||
ansible.builtin.copy:
|
||||
content: "{{ _join_command.stdout }}\n"
|
||||
dest: /etc/kubernetes/worker_join_command.sh
|
||||
mode: "0600"
|
||||
|
||||
# Fix #3: create .kube dir on managers before copying kubeconfig (race condition)
|
||||
- name: kubeadm init | Ensure .kube dir on manager nodes
|
||||
ansible.builtin.file:
|
||||
path: "/home/{{ ansible_user }}/.kube"
|
||||
state: directory
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: "0700"
|
||||
delegate_to: "{{ item }}"
|
||||
loop: "{{ groups['manager_nodes'] }}"
|
||||
when: kubeconfig_fetch_to_managers | bool
|
||||
|
||||
- name: kubeadm init | Fetch kubeconfig to controller
|
||||
ansible.builtin.fetch:
|
||||
src: /etc/kubernetes/admin.conf
|
||||
dest: "/tmp/k8s_admin_{{ inventory_hostname }}.conf"
|
||||
flat: true
|
||||
when: kubeconfig_fetch_to_managers | bool
|
||||
|
||||
- name: kubeadm init | Distribute kubeconfig to manager nodes
|
||||
ansible.builtin.copy:
|
||||
src: "/tmp/k8s_admin_{{ inventory_hostname }}.conf"
|
||||
dest: "/home/{{ ansible_user }}/.kube/config"
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: "0600"
|
||||
delegate_to: "{{ item }}"
|
||||
loop: "{{ groups['manager_nodes'] }}"
|
||||
when: kubeconfig_fetch_to_managers | bool
|
||||
34
roles/k8s_control_plane/tasks/kubernetes.yml
Normal file
34
roles/k8s_control_plane/tasks/kubernetes.yml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
- name: Kubernetes | Add repo
|
||||
ansible.builtin.yum_repository:
|
||||
name: kubernetes
|
||||
description: Kubernetes
|
||||
baseurl: "https://pkgs.k8s.io/core:/stable:/v{{ k8s_version }}/rpm/"
|
||||
enabled: true
|
||||
gpgcheck: true
|
||||
gpgkey: "https://pkgs.k8s.io/core:/stable:/v{{ k8s_version }}/rpm/repodata/repomd.xml.key"
|
||||
exclude: "kubelet kubeadm kubectl cri-tools kubernetes-cni"
|
||||
timeout: "300"
|
||||
|
||||
- name: Kubernetes | Set a low but nonzero minrate so a truly stalled CDN connection times out
|
||||
community.general.ini_file:
|
||||
path: /etc/yum.repos.d/kubernetes.repo
|
||||
section: kubernetes
|
||||
option: minrate
|
||||
value: "1000"
|
||||
mode: "0644"
|
||||
|
||||
- name: Kubernetes | Install kubelet, kubeadm, kubectl
|
||||
ansible.builtin.dnf:
|
||||
name:
|
||||
- kubelet
|
||||
- kubeadm
|
||||
- kubectl
|
||||
state: present
|
||||
disable_excludes: kubernetes
|
||||
|
||||
- name: Kubernetes | Enable kubelet (kubeadm will start it)
|
||||
ansible.builtin.systemd:
|
||||
name: kubelet
|
||||
enabled: true
|
||||
daemon_reload: true
|
||||
12
roles/k8s_control_plane/tasks/main.yml
Normal file
12
roles/k8s_control_plane/tasks/main.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
- name: Prerequisites
|
||||
ansible.builtin.include_tasks: prerequisites.yml
|
||||
|
||||
- name: Containerd
|
||||
ansible.builtin.include_tasks: containerd.yml
|
||||
|
||||
- name: Kubernetes packages
|
||||
ansible.builtin.include_tasks: kubernetes.yml
|
||||
|
||||
- name: Cluster init
|
||||
ansible.builtin.include_tasks: kubeadm_init.yml
|
||||
104
roles/k8s_control_plane/tasks/prerequisites.yml
Normal file
104
roles/k8s_control_plane/tasks/prerequisites.yml
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
- name: Prerequisites | Set hostname
|
||||
ansible.builtin.hostname:
|
||||
name: "{{ inventory_hostname }}"
|
||||
|
||||
- name: Prerequisites | Add hostname to /etc/hosts
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/hosts
|
||||
line: "{{ ansible_host }} {{ inventory_hostname }}"
|
||||
regexp: ".*{{ inventory_hostname }}$"
|
||||
state: present
|
||||
|
||||
- name: Prerequisites | Disable swap permanently
|
||||
ansible.builtin.replace:
|
||||
path: /etc/fstab
|
||||
regexp: '^([^#].*\s+swap\s+.*)$'
|
||||
replace: '# \1'
|
||||
|
||||
- name: Prerequisites | Disable swap now
|
||||
ansible.builtin.command: swapoff -a
|
||||
changed_when: false
|
||||
|
||||
- name: Prerequisites | Set SELinux to permissive
|
||||
ansible.posix.selinux:
|
||||
policy: targeted
|
||||
state: permissive
|
||||
|
||||
- name: Prerequisites | Load kernel modules (persistent)
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/modules-load.d/k8s.conf
|
||||
content: |
|
||||
overlay
|
||||
br_netfilter
|
||||
mode: "0644"
|
||||
|
||||
- name: Prerequisites | Load kernel modules now
|
||||
community.general.modprobe:
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
loop:
|
||||
- overlay
|
||||
- br_netfilter
|
||||
|
||||
- name: Prerequisites | Set sysctl params
|
||||
ansible.posix.sysctl:
|
||||
name: "{{ item.key }}"
|
||||
value: "{{ item.value }}"
|
||||
sysctl_file: /etc/sysctl.d/k8s.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" }
|
||||
|
||||
# Fix #9: ensure firewalld is running before opening ports
|
||||
- name: Prerequisites | Ensure firewalld is running
|
||||
ansible.builtin.systemd:
|
||||
name: firewalld
|
||||
state: started
|
||||
enabled: true
|
||||
daemon_reload: true
|
||||
|
||||
- name: Prerequisites | Open control plane firewall ports
|
||||
ansible.posix.firewalld:
|
||||
port: "{{ item }}"
|
||||
permanent: true
|
||||
state: enabled
|
||||
immediate: "{{ not ansible_check_mode }}"
|
||||
loop:
|
||||
- 6443/tcp # kube-apiserver
|
||||
- 2379-2380/tcp # etcd
|
||||
- 10250/tcp # kubelet API
|
||||
- 10251/tcp # kube-scheduler
|
||||
- 10252/tcp # kube-controller-manager
|
||||
- 10257/tcp # kube-controller-manager (secure)
|
||||
- 10259/tcp # kube-scheduler (secure)
|
||||
- 8472/udp # Flannel VXLAN (fix #4: required for pod-to-pod traffic across nodes)
|
||||
|
||||
- name: Prerequisites | Trust overlay interfaces in firewalld
|
||||
ansible.posix.firewalld:
|
||||
interface: "{{ item }}"
|
||||
zone: trusted
|
||||
permanent: true
|
||||
state: enabled
|
||||
immediate: "{{ not ansible_check_mode }}"
|
||||
loop:
|
||||
- flannel.1
|
||||
- cni0
|
||||
|
||||
# Interface-based rules above only work when the interface already exists at
|
||||
# firewalld reload time. Flannel creates flannel.1/cni0 dynamically (no
|
||||
# NetworkManager integration), so they may not be in the trusted zone after
|
||||
# a fresh boot. Source-based CIDR rules are reliable regardless of interface
|
||||
# lifecycle and cover all pod-to-pod and service traffic.
|
||||
- name: Prerequisites | Trust pod and service CIDRs in firewalld
|
||||
ansible.posix.firewalld:
|
||||
source: "{{ item }}"
|
||||
zone: trusted
|
||||
permanent: true
|
||||
state: enabled
|
||||
immediate: "{{ not ansible_check_mode }}"
|
||||
loop:
|
||||
- "{{ pod_network_cidr }}"
|
||||
- "{{ service_cidr }}"
|
||||
20
roles/k8s_control_plane/templates/kubeadm_config.yml.j2
Normal file
20
roles/k8s_control_plane/templates/kubeadm_config.yml.j2
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
apiVersion: kubeadm.k8s.io/v1beta4
|
||||
kind: InitConfiguration
|
||||
localAPIEndpoint:
|
||||
advertiseAddress: "{{ apiserver_advertise_address }}"
|
||||
bindPort: 6443
|
||||
nodeRegistration:
|
||||
criSocket: unix:///run/containerd/containerd.sock
|
||||
name: "{{ inventory_hostname }}"
|
||||
---
|
||||
apiVersion: kubeadm.k8s.io/v1beta4
|
||||
kind: ClusterConfiguration
|
||||
kubernetesVersion: "{{ _kubeadm_full_version.stdout | trim }}"
|
||||
networking:
|
||||
podSubnet: "{{ pod_network_cidr }}"
|
||||
serviceSubnet: "{{ service_cidr }}"
|
||||
---
|
||||
apiVersion: kubelet.config.k8s.io/v1beta1
|
||||
kind: KubeletConfiguration
|
||||
cgroupDriver: systemd
|
||||
18
roles/k8s_gbm_dev/defaults/main.yml
Normal file
18
roles/k8s_gbm_dev/defaults/main.yml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
dev_access_namespace: gigacom-billing-mobile
|
||||
dev_access_sa_name: developer
|
||||
dev_access_role_name: developer-role
|
||||
dev_access_rolebinding_name: developer-rolebinding
|
||||
dev_access_token_secret_name: developer-token
|
||||
dev_access_token_file: "/home/{{ ansible_user }}/.kube/dev-token-{{ dev_access_namespace }}"
|
||||
|
||||
dev_access_kubeconfig: "/home/{{ ansible_user }}/.kube/config"
|
||||
|
||||
# Verbs granted on namespace resources
|
||||
dev_access_verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
|
||||
# Whether to also allow exec/portforward/logs (useful for debugging)
|
||||
dev_access_allow_exec: true
|
||||
16
roles/k8s_gbm_dev/tasks/main.yml
Normal file
16
roles/k8s_gbm_dev/tasks/main.yml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
- name: DevAccess | Check kubeconfig exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ dev_access_kubeconfig }}"
|
||||
register: _kubeconfig_dev
|
||||
|
||||
- name: DevAccess | Fail if kubeconfig missing
|
||||
ansible.builtin.fail:
|
||||
msg: "kubeconfig not found at {{ dev_access_kubeconfig }}"
|
||||
when: not _kubeconfig_dev.stat.exists
|
||||
|
||||
- name: DevAccess | Apply RBAC
|
||||
ansible.builtin.include_tasks: rbac.yml
|
||||
|
||||
- name: DevAccess | Create token
|
||||
ansible.builtin.include_tasks: token.yml
|
||||
57
roles/k8s_gbm_dev/tasks/rbac.yml
Normal file
57
roles/k8s_gbm_dev/tasks/rbac.yml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
- name: DevAccess | Render RBAC manifests
|
||||
ansible.builtin.template:
|
||||
src: "{{ item.src }}"
|
||||
dest: "/tmp/{{ item.dest }}"
|
||||
mode: "0600"
|
||||
loop:
|
||||
- { src: dev-serviceaccount.yml.j2, dest: dev-sa.yml }
|
||||
- { src: dev-role.yml.j2, dest: dev-role.yml }
|
||||
- { src: dev-rolebinding.yml.j2, dest: dev-rolebinding.yml }
|
||||
- { src: dev-clusterrole.yml.j2, dest: dev-clusterrole.yml }
|
||||
- { src: dev-clusterrolebinding.yml.j2, dest: dev-clusterrolebinding.yml }
|
||||
|
||||
- name: DevAccess | Apply ServiceAccount
|
||||
ansible.builtin.command:
|
||||
cmd: kubectl apply -f /tmp/dev-sa.yml
|
||||
environment:
|
||||
KUBECONFIG: "{{ dev_access_kubeconfig }}"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
register: _dev_sa
|
||||
changed_when: "'created' in _dev_sa.stdout or 'configured' in _dev_sa.stdout"
|
||||
|
||||
- name: DevAccess | Apply Role
|
||||
ansible.builtin.command:
|
||||
cmd: kubectl apply -f /tmp/dev-role.yml
|
||||
environment:
|
||||
KUBECONFIG: "{{ dev_access_kubeconfig }}"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
register: _dev_role
|
||||
changed_when: "'created' in _dev_role.stdout or 'configured' in _dev_role.stdout"
|
||||
|
||||
- name: DevAccess | Apply RoleBinding
|
||||
ansible.builtin.command:
|
||||
cmd: kubectl apply -f /tmp/dev-rolebinding.yml
|
||||
environment:
|
||||
KUBECONFIG: "{{ dev_access_kubeconfig }}"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
register: _dev_rb
|
||||
changed_when: "'created' in _dev_rb.stdout or 'configured' in _dev_rb.stdout"
|
||||
|
||||
- name: DevAccess | Apply ClusterRole (namespace reader)
|
||||
ansible.builtin.command:
|
||||
cmd: kubectl apply -f /tmp/dev-clusterrole.yml
|
||||
environment:
|
||||
KUBECONFIG: "{{ dev_access_kubeconfig }}"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
register: _dev_cr
|
||||
changed_when: "'created' in _dev_cr.stdout or 'configured' in _dev_cr.stdout"
|
||||
|
||||
- name: DevAccess | Apply ClusterRoleBinding (namespace reader)
|
||||
ansible.builtin.command:
|
||||
cmd: kubectl apply -f /tmp/dev-clusterrolebinding.yml
|
||||
environment:
|
||||
KUBECONFIG: "{{ dev_access_kubeconfig }}"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
register: _dev_crb
|
||||
changed_when: "'created' in _dev_crb.stdout or 'configured' in _dev_crb.stdout"
|
||||
54
roles/k8s_gbm_dev/tasks/token.yml
Normal file
54
roles/k8s_gbm_dev/tasks/token.yml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
- name: DevAccess | Render token Secret manifest
|
||||
ansible.builtin.template:
|
||||
src: dev-token-secret.yml.j2
|
||||
dest: /tmp/dev-token-secret.yml
|
||||
mode: "0600"
|
||||
|
||||
- name: DevAccess | Apply token Secret
|
||||
ansible.builtin.command:
|
||||
cmd: kubectl apply -f /tmp/dev-token-secret.yml
|
||||
environment:
|
||||
KUBECONFIG: "{{ dev_access_kubeconfig }}"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
register: _dev_secret
|
||||
changed_when: "'created' in _dev_secret.stdout or 'configured' in _dev_secret.stdout"
|
||||
|
||||
- name: DevAccess | Wait for token to be populated
|
||||
ansible.builtin.command: >
|
||||
kubectl get secret {{ dev_access_token_secret_name }}
|
||||
-n {{ dev_access_namespace }}
|
||||
-o jsonpath='{.data.token}'
|
||||
environment:
|
||||
KUBECONFIG: "{{ dev_access_kubeconfig }}"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
register: _token_raw
|
||||
retries: 10
|
||||
delay: 3
|
||||
until: _token_raw.stdout | length > 0
|
||||
changed_when: false
|
||||
|
||||
- name: DevAccess | Decode token
|
||||
ansible.builtin.set_fact:
|
||||
_dev_token: "{{ _token_raw.stdout | b64decode }}"
|
||||
|
||||
- name: DevAccess | Save token to file
|
||||
ansible.builtin.copy:
|
||||
content: "{{ _dev_token }}\n"
|
||||
dest: "{{ dev_access_token_file }}"
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: "0600"
|
||||
|
||||
- name: DevAccess | Show token info
|
||||
ansible.builtin.debug:
|
||||
msg: |
|
||||
============================================================
|
||||
Developer token for namespace: {{ dev_access_namespace }}
|
||||
ServiceAccount: {{ dev_access_sa_name }}
|
||||
Token saved to: {{ dev_access_token_file }}
|
||||
View token: cat {{ dev_access_token_file }}
|
||||
|
||||
Dashboard URL: http://10.203.0.96:10001
|
||||
Login: paste token from the file above
|
||||
============================================================
|
||||
11
roles/k8s_gbm_dev/templates/dev-clusterrole.yml.j2
Normal file
11
roles/k8s_gbm_dev/templates/dev-clusterrole.yml.j2
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
# Minimal cluster-level read for Dashboard namespace selector.
|
||||
# Without namespaces/list the Dashboard shows nothing after login.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: {{ dev_access_sa_name }}-namespace-reader
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["namespaces"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
13
roles/k8s_gbm_dev/templates/dev-clusterrolebinding.yml.j2
Normal file
13
roles/k8s_gbm_dev/templates/dev-clusterrolebinding.yml.j2
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: {{ dev_access_sa_name }}-namespace-reader
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: {{ dev_access_sa_name }}-namespace-reader
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ dev_access_sa_name }}
|
||||
namespace: {{ dev_access_namespace }}
|
||||
63
roles/k8s_gbm_dev/templates/dev-role.yml.j2
Normal file
63
roles/k8s_gbm_dev/templates/dev-role.yml.j2
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: {{ dev_access_role_name }}
|
||||
namespace: {{ dev_access_namespace }}
|
||||
rules:
|
||||
# Core workload resources — read
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- pods
|
||||
- services
|
||||
- endpoints
|
||||
- configmaps
|
||||
- persistentvolumeclaims
|
||||
- events
|
||||
- replicationcontrollers
|
||||
verbs: {{ dev_access_verbs | to_json }}
|
||||
# Logs — always readable
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- pods/log
|
||||
verbs: ["get", "list", "watch"]
|
||||
{% if dev_access_allow_exec | bool %}
|
||||
# Exec and port-forward — for debugging
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- pods/exec
|
||||
- pods/portforward
|
||||
verbs: ["create"]
|
||||
{% endif %}
|
||||
# Secrets — read only (developers may need env inspection)
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- secrets
|
||||
verbs: ["get", "list"]
|
||||
# Apps
|
||||
- apiGroups: ["apps"]
|
||||
resources:
|
||||
- deployments
|
||||
- replicasets
|
||||
- statefulsets
|
||||
- daemonsets
|
||||
verbs: {{ dev_access_verbs | to_json }}
|
||||
# Batch
|
||||
- apiGroups: ["batch"]
|
||||
resources:
|
||||
- jobs
|
||||
- cronjobs
|
||||
verbs: {{ dev_access_verbs | to_json }}
|
||||
# Networking
|
||||
- apiGroups: ["networking.k8s.io"]
|
||||
resources:
|
||||
- ingresses
|
||||
- networkpolicies
|
||||
verbs: {{ dev_access_verbs | to_json }}
|
||||
# Traefik CRDs
|
||||
- apiGroups: ["traefik.io"]
|
||||
resources:
|
||||
- ingressroutes
|
||||
- ingressroutetcps
|
||||
- middlewares
|
||||
verbs: {{ dev_access_verbs | to_json }}
|
||||
14
roles/k8s_gbm_dev/templates/dev-rolebinding.yml.j2
Normal file
14
roles/k8s_gbm_dev/templates/dev-rolebinding.yml.j2
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: {{ dev_access_rolebinding_name }}
|
||||
namespace: {{ dev_access_namespace }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: {{ dev_access_role_name }}
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ dev_access_sa_name }}
|
||||
namespace: {{ dev_access_namespace }}
|
||||
6
roles/k8s_gbm_dev/templates/dev-serviceaccount.yml.j2
Normal file
6
roles/k8s_gbm_dev/templates/dev-serviceaccount.yml.j2
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ dev_access_sa_name }}
|
||||
namespace: {{ dev_access_namespace }}
|
||||
10
roles/k8s_gbm_dev/templates/dev-token-secret.yml.j2
Normal file
10
roles/k8s_gbm_dev/templates/dev-token-secret.yml.j2
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
# Long-lived token for ServiceAccount (Kubernetes 1.24+ requires explicit Secret)
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ dev_access_token_secret_name }}
|
||||
namespace: {{ dev_access_namespace }}
|
||||
annotations:
|
||||
kubernetes.io/service-account.name: {{ dev_access_sa_name }}
|
||||
type: kubernetes.io/service-account-token
|
||||
32
roles/k8s_manager/defaults/main.yml
Normal file
32
roles/k8s_manager/defaults/main.yml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
k8s_manager_timezone: "Europe/Moscow"
|
||||
|
||||
# Fix #8: opt-in full upgrade — set true only when intentionally patching the OS
|
||||
k8s_manager_upgrade_packages: false
|
||||
|
||||
kubectl_version: ""
|
||||
helm_version: ""
|
||||
k9s_version: ""
|
||||
|
||||
k8s_manager_extra_packages:
|
||||
- bash-completion
|
||||
- curl
|
||||
- wget
|
||||
- git
|
||||
- vim
|
||||
- htop
|
||||
- net-tools
|
||||
- bind-utils
|
||||
- jq
|
||||
- python3
|
||||
- python3-pip
|
||||
|
||||
# kubeconfig location on the manager node
|
||||
kubeconfig_dir: "/home/{{ ansible_user }}/.kube"
|
||||
|
||||
# Kubernetes Dashboard (manifest from GitHub raw — no CDN issues)
|
||||
dashboard_enabled: true
|
||||
dashboard_namespace: "kubernetes-dashboard"
|
||||
dashboard_manifest_url: "https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml"
|
||||
# Traefik terminates TLS — dashboard serves plain HTTP on port 9090
|
||||
dashboard_insecure: false
|
||||
6
roles/k8s_manager/handlers/main.yml
Normal file
6
roles/k8s_manager/handlers/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: reload bashrc
|
||||
ansible.builtin.command: source /etc/bashrc
|
||||
args:
|
||||
executable: /bin/bash
|
||||
become: false
|
||||
11
roles/k8s_manager/meta/main.yml
Normal file
11
roles/k8s_manager/meta/main.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
galaxy_info:
|
||||
author: ops
|
||||
description: Setup Kubernetes management node on Rocky Linux 9
|
||||
license: MIT
|
||||
min_ansible_version: "2.14"
|
||||
platforms:
|
||||
- name: EL
|
||||
versions:
|
||||
- "9"
|
||||
dependencies: []
|
||||
91
roles/k8s_manager/tasks/dashboard.yml
Normal file
91
roles/k8s_manager/tasks/dashboard.yml
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
- name: Dashboard | Check kubeconfig exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ kubeconfig_dir }}/config"
|
||||
register: _kubeconfig_dash
|
||||
|
||||
- name: Dashboard | Skip notice
|
||||
ansible.builtin.debug:
|
||||
msg: "Dashboard install skipped — kubeconfig not found at {{ kubeconfig_dir }}/config."
|
||||
when: not _kubeconfig_dash.stat.exists
|
||||
|
||||
- name: Dashboard | Deploy and configure
|
||||
when:
|
||||
- _kubeconfig_dash.stat.exists
|
||||
- dashboard_enabled | bool
|
||||
environment:
|
||||
KUBECONFIG: "{{ kubeconfig_dir }}/config"
|
||||
PATH: "/usr/local/bin:/usr/bin:/bin"
|
||||
block:
|
||||
- name: Dashboard | Apply manifest
|
||||
ansible.builtin.command:
|
||||
cmd: kubectl apply -f {{ dashboard_manifest_url }}
|
||||
register: _dash_apply
|
||||
changed_when: "'configured' in _dash_apply.stdout or 'created' in _dash_apply.stdout"
|
||||
|
||||
- name: Dashboard | Patch deployment for insecure HTTP access
|
||||
ansible.builtin.command: >
|
||||
kubectl patch deployment kubernetes-dashboard
|
||||
-n {{ dashboard_namespace }}
|
||||
--type strategic
|
||||
-p '{"spec":{"template":{"spec":{"containers":[{"name":"kubernetes-dashboard","args":["--namespace={{ dashboard_namespace }}","--enable-insecure-login","--insecure-port=9090","--port=0"],"ports":[{"containerPort":9090,"protocol":"TCP"}],"livenessProbe":{"httpGet":{"path":"/","port":9090,"scheme":"HTTP"}}}]}}}}'
|
||||
register: _dash_deploy_patch
|
||||
changed_when: "'patched' in _dash_deploy_patch.stdout"
|
||||
failed_when: _dash_deploy_patch.rc != 0 and 'unchanged' not in _dash_deploy_patch.stdout
|
||||
when: dashboard_insecure | bool
|
||||
check_mode: false
|
||||
|
||||
- name: Dashboard | Patch service to ClusterIP
|
||||
ansible.builtin.command: >
|
||||
kubectl patch svc kubernetes-dashboard
|
||||
-n {{ dashboard_namespace }}
|
||||
-p '{"spec":{"type":"ClusterIP","ports":[{"port":443,"targetPort":{{ 9090 if dashboard_insecure | bool else 8443 }},"protocol":"TCP"}]}}'
|
||||
register: _dash_patch
|
||||
changed_when: "'patched' in _dash_patch.stdout"
|
||||
failed_when: _dash_patch.rc != 0 and 'not patched' not in _dash_patch.stdout
|
||||
check_mode: false
|
||||
|
||||
- name: Dashboard | Apply admin ServiceAccount
|
||||
ansible.builtin.template:
|
||||
src: dashboard_admin.yml.j2
|
||||
dest: /tmp/dashboard_admin.yml
|
||||
mode: "0600"
|
||||
|
||||
- name: Dashboard | Create admin user
|
||||
ansible.builtin.command:
|
||||
cmd: kubectl apply -f /tmp/dashboard_admin.yml
|
||||
register: _dash_admin
|
||||
changed_when: "'created' in _dash_admin.stdout"
|
||||
|
||||
- name: Dashboard | Check if token file exists
|
||||
ansible.builtin.stat:
|
||||
path: "/home/{{ ansible_user }}/.kube/dashboard-token"
|
||||
register: _token_file
|
||||
|
||||
- name: Dashboard | Generate long-lived token
|
||||
ansible.builtin.command:
|
||||
cmd: kubectl -n {{ dashboard_namespace }} create token admin-user --duration=8760h
|
||||
register: _dashboard_token
|
||||
changed_when: true
|
||||
when: not _token_file.stat.exists
|
||||
|
||||
- name: Dashboard | Save token to file
|
||||
ansible.builtin.copy:
|
||||
content: "{{ _dashboard_token.stdout }}\n"
|
||||
dest: "/home/{{ ansible_user }}/.kube/dashboard-token"
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: "0600"
|
||||
when: not _token_file.stat.exists
|
||||
|
||||
- name: Dashboard | Show access info
|
||||
ansible.builtin.debug:
|
||||
msg: |
|
||||
============================================================
|
||||
Kubernetes Dashboard: accessible via Traefik proxy.
|
||||
{% if dashboard_insecure | bool %}
|
||||
Mode: HTTP (insecure) — TLS terminated by Traefik
|
||||
{% endif %}
|
||||
Token saved to: ~/.kube/dashboard-token
|
||||
View token: cat ~/.kube/dashboard-token
|
||||
============================================================
|
||||
133
roles/k8s_manager/tasks/main.yml
Normal file
133
roles/k8s_manager/tasks/main.yml
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
---
|
||||
- name: System | Set hostname
|
||||
ansible.builtin.hostname:
|
||||
name: "{{ inventory_hostname }}"
|
||||
|
||||
- name: System | Set timezone
|
||||
community.general.timezone:
|
||||
name: "{{ k8s_manager_timezone }}"
|
||||
|
||||
# Fix #8: explicit full upgrade is opt-in (k8s_manager_upgrade_packages: true)
|
||||
# to prevent unintended kernel/system updates on prod runs
|
||||
- name: System | Update cache
|
||||
ansible.builtin.dnf:
|
||||
update_cache: true
|
||||
|
||||
- name: System | Upgrade all packages # noqa: package-latest
|
||||
ansible.builtin.dnf:
|
||||
name: "*"
|
||||
state: latest
|
||||
when: k8s_manager_upgrade_packages | bool
|
||||
|
||||
- name: System | Install extra packages
|
||||
ansible.builtin.dnf:
|
||||
name: "{{ k8s_manager_extra_packages }}"
|
||||
state: present
|
||||
|
||||
- name: kubectl | Get latest stable version
|
||||
ansible.builtin.uri:
|
||||
url: https://dl.k8s.io/release/stable.txt
|
||||
return_content: true
|
||||
register: _kubectl_latest
|
||||
when: not kubectl_version
|
||||
check_mode: false
|
||||
|
||||
- name: kubectl | Set version fact (latest)
|
||||
ansible.builtin.set_fact:
|
||||
_kubectl_version: "{{ _kubectl_latest.content | trim }}"
|
||||
when: not kubectl_version
|
||||
|
||||
- name: kubectl | Set version fact (pinned)
|
||||
ansible.builtin.set_fact:
|
||||
_kubectl_version: "{{ kubectl_version }}"
|
||||
when: kubectl_version
|
||||
|
||||
- name: kubectl | Download binary
|
||||
ansible.builtin.get_url:
|
||||
url: "https://dl.k8s.io/release/{{ _kubectl_version }}/bin/linux/amd64/kubectl"
|
||||
dest: /usr/local/bin/kubectl
|
||||
mode: "0755"
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: kubectl | Enable bash completion
|
||||
ansible.builtin.shell: /usr/local/bin/kubectl completion bash > /etc/bash_completion.d/kubectl
|
||||
args:
|
||||
creates: /etc/bash_completion.d/kubectl
|
||||
|
||||
- name: helm | Install EPEL repository
|
||||
ansible.builtin.dnf:
|
||||
name: epel-release
|
||||
state: present
|
||||
|
||||
- name: helm | Install via DNF
|
||||
ansible.builtin.dnf:
|
||||
name: helm
|
||||
state: present
|
||||
|
||||
- name: helm | Enable bash completion
|
||||
ansible.builtin.shell: helm completion bash > /etc/bash_completion.d/helm
|
||||
args:
|
||||
creates: /etc/bash_completion.d/helm
|
||||
|
||||
- name: k9s | Install via DNF
|
||||
ansible.builtin.dnf:
|
||||
name: "https://github.com/derailed/k9s/releases/latest/download/k9s_linux_amd64.rpm"
|
||||
state: present
|
||||
disable_gpg_check: true
|
||||
|
||||
|
||||
- name: k9s | Set permissions
|
||||
ansible.builtin.file:
|
||||
path: /usr/local/bin/k9s
|
||||
mode: "0755"
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
- name: kubectx/kubens | Download kubectx
|
||||
ansible.builtin.get_url:
|
||||
url: https://raw.githubusercontent.com/ahmetb/kubectx/master/kubectx
|
||||
dest: '{{ role_path }}/files/kubectx'
|
||||
mode: "0755"
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
- name: kubectx/kubens | Download kubens
|
||||
ansible.builtin.get_url:
|
||||
url: https://raw.githubusercontent.com/ahmetb/kubectx/master/kubens
|
||||
dest: '{{ role_path }}/files/kubens'
|
||||
mode: "0755"
|
||||
delegate_to: localhost
|
||||
run_once: true
|
||||
|
||||
- name: kubectx/kubens | Install binaries
|
||||
ansible.builtin.copy:
|
||||
src: "{{ role_path }}/files/{{ item }}"
|
||||
dest: /usr/local/bin/{{ item }}
|
||||
mode: "0755"
|
||||
owner: root
|
||||
group: root
|
||||
loop:
|
||||
- kubectx
|
||||
- kubens
|
||||
|
||||
- name: kubeconfig | Ensure .kube directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ kubeconfig_dir }}"
|
||||
state: directory
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: "0700"
|
||||
|
||||
- name: dashboard | Install Kubernetes Dashboard
|
||||
ansible.builtin.include_tasks: dashboard.yml
|
||||
|
||||
- name: bash | Add kubectl alias and kubeconfig to profile
|
||||
ansible.builtin.blockinfile:
|
||||
path: "/home/{{ ansible_user }}/.bashrc"
|
||||
marker: "# {mark} ANSIBLE MANAGED — k8s aliases"
|
||||
block: |
|
||||
export KUBECONFIG="{{ kubeconfig_dir }}/config"
|
||||
source /usr/share/bash-completion/bash_completion
|
||||
alias k=kubectl
|
||||
complete -o default -F __start_kubectl k
|
||||
19
roles/k8s_manager/templates/dashboard_admin.yml.j2
Normal file
19
roles/k8s_manager/templates/dashboard_admin.yml.j2
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: admin-user
|
||||
namespace: {{ dashboard_namespace }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: admin-user
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: admin-user
|
||||
namespace: {{ dashboard_namespace }}
|
||||
6
roles/k8s_worker/defaults/main.yml
Normal file
6
roles/k8s_worker/defaults/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
k8s_version: "1.33"
|
||||
containerd_version: "" # empty = latest from Docker CE repo
|
||||
|
||||
# Control plane host to generate the join command from
|
||||
worker_join_source_host: "{{ groups['control_plane'][0] }}"
|
||||
12
roles/k8s_worker/handlers/main.yml
Normal file
12
roles/k8s_worker/handlers/main.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
- name: Restart containerd
|
||||
ansible.builtin.systemd:
|
||||
name: containerd
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
|
||||
- name: Restart kubelet
|
||||
ansible.builtin.systemd:
|
||||
name: kubelet
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
11
roles/k8s_worker/meta/main.yml
Normal file
11
roles/k8s_worker/meta/main.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
galaxy_info:
|
||||
author: ops
|
||||
description: Kubernetes worker node on Rocky Linux 9
|
||||
license: MIT
|
||||
min_ansible_version: "2.14"
|
||||
platforms:
|
||||
- name: EL
|
||||
versions:
|
||||
- "9"
|
||||
dependencies: []
|
||||
57
roles/k8s_worker/tasks/containerd.yml
Normal file
57
roles/k8s_worker/tasks/containerd.yml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
- name: Containerd | Add Docker CE repo
|
||||
ansible.builtin.get_url:
|
||||
url: https://download.docker.com/linux/rhel/docker-ce.repo
|
||||
dest: /etc/yum.repos.d/docker-ce.repo
|
||||
mode: "0644"
|
||||
|
||||
- name: Containerd | Disable conflicting container-tools module
|
||||
ansible.builtin.command: dnf module disable container-tools -y
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Containerd | Install containerd.io
|
||||
ansible.builtin.dnf:
|
||||
name: "{{ 'containerd.io-' + containerd_version if containerd_version != '' else 'containerd.io' }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
disablerepo: kubernetes
|
||||
|
||||
- name: Containerd | Ensure runc is available at path expected by containerd
|
||||
ansible.builtin.shell: |
|
||||
RUNC_BIN=$(which runc 2>/dev/null || echo "")
|
||||
if [ -n "$RUNC_BIN" ] && [ ! -f /usr/local/bin/runc ]; then
|
||||
ln -sf "$RUNC_BIN" /usr/local/bin/runc
|
||||
fi
|
||||
changed_when: false
|
||||
|
||||
- name: Containerd | Remove default config
|
||||
ansible.builtin.file:
|
||||
dest: /etc/containerd/config.toml
|
||||
state: absent
|
||||
|
||||
- name: Containerd | Generate default config
|
||||
ansible.builtin.shell: containerd config default > /etc/containerd/config.toml
|
||||
args:
|
||||
creates: /etc/containerd/config.toml
|
||||
|
||||
- name: Containerd | Enable SystemdCgroup
|
||||
ansible.builtin.replace:
|
||||
path: /etc/containerd/config.toml
|
||||
regexp: 'SystemdCgroup = false'
|
||||
replace: 'SystemdCgroup = true'
|
||||
notify: Restart containerd
|
||||
|
||||
- name: Containerd | Enable SystemdCgroup
|
||||
ansible.builtin.replace:
|
||||
path: /etc/containerd/config.toml
|
||||
regexp: 'disabled_plugins = ["cri"]'
|
||||
replace: 'disabled_plugins = []'
|
||||
notify: restart containerd
|
||||
|
||||
- name: Containerd | Enable and start service
|
||||
ansible.builtin.systemd:
|
||||
name: containerd
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
19
roles/k8s_worker/tasks/kubeadm_join.yml
Normal file
19
roles/k8s_worker/tasks/kubeadm_join.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
- name: kubeadm join | Check if node already joined
|
||||
ansible.builtin.stat:
|
||||
path: /etc/kubernetes/kubelet.conf
|
||||
register: _kubelet_conf
|
||||
|
||||
- name: kubeadm join | Generate fresh join command on control plane
|
||||
ansible.builtin.command: kubeadm token create --print-join-command
|
||||
delegate_to: "{{ worker_join_source_host }}"
|
||||
register: _join_command
|
||||
changed_when: false
|
||||
when: not _kubelet_conf.stat.exists
|
||||
|
||||
- name: kubeadm join | Execute join command
|
||||
ansible.builtin.command: "{{ _join_command.stdout | trim }}"
|
||||
when: not _kubelet_conf.stat.exists
|
||||
changed_when: true
|
||||
async: 300
|
||||
poll: 10
|
||||
33
roles/k8s_worker/tasks/kubernetes.yml
Normal file
33
roles/k8s_worker/tasks/kubernetes.yml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
- name: Kubernetes | Add repo
|
||||
ansible.builtin.yum_repository:
|
||||
name: kubernetes
|
||||
description: Kubernetes
|
||||
baseurl: "https://pkgs.k8s.io/core:/stable:/v{{ k8s_version }}/rpm/"
|
||||
enabled: true
|
||||
gpgcheck: true
|
||||
gpgkey: "https://pkgs.k8s.io/core:/stable:/v{{ k8s_version }}/rpm/repodata/repomd.xml.key"
|
||||
exclude: "kubelet kubeadm kubectl cri-tools kubernetes-cni"
|
||||
timeout: "300"
|
||||
|
||||
- name: Kubernetes | Set a low but nonzero minrate so a truly stalled CDN connection times out
|
||||
community.general.ini_file:
|
||||
path: /etc/yum.repos.d/kubernetes.repo
|
||||
section: kubernetes
|
||||
option: minrate
|
||||
value: "1000"
|
||||
mode: "0644"
|
||||
|
||||
- name: Kubernetes | Install kubelet and kubeadm
|
||||
ansible.builtin.dnf:
|
||||
name:
|
||||
- kubelet
|
||||
- kubeadm
|
||||
state: present
|
||||
disable_excludes: kubernetes
|
||||
|
||||
- name: Kubernetes | Enable kubelet (kubeadm join will start it)
|
||||
ansible.builtin.systemd:
|
||||
name: kubelet
|
||||
enabled: true
|
||||
daemon_reload: true
|
||||
12
roles/k8s_worker/tasks/main.yml
Normal file
12
roles/k8s_worker/tasks/main.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
- name: Prerequisites
|
||||
ansible.builtin.include_tasks: prerequisites.yml
|
||||
|
||||
- name: Containerd
|
||||
ansible.builtin.include_tasks: containerd.yml
|
||||
|
||||
- name: Kubernetes packages
|
||||
ansible.builtin.include_tasks: kubernetes.yml
|
||||
|
||||
- name: Join cluster
|
||||
ansible.builtin.include_tasks: kubeadm_join.yml
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue