feat: добавлены роли mdadm и k8s-user с полной оркестрацией SSH ключей

Роль mdadm:
- автоопределение RAID массива через mdadm --detail --scan
- монтирование в /storage через fstab (UUID-based, nofail)
- автоопределение fstype через blkid
- обновление mdadm.conf + initramfs
- флаг mdadm_enabled для отключения на отдельных нодах

Роль k8s-user:
- создание пользователя k8s + группа + sudo без пароля (visudo validation)
- генерация RSA 4096 ключевой пары на первом мастере (идемпотентно, creates:)
- раскладка приватного и публичного ключа на все ноды кластера
- добавление public key в authorized_keys — SSH с любой ноды на любую
- обновление /etc/hosts блоками через blockinfile (k3s_cluster + lab_hosts)
- поддержка lab_hosts: создание пользователя и деплой ключей через пароль из vault

Плейбуки:
- k8s-user.yml — полная оркестрация (5 plays: create → generate → distribute → hosts → lab)
- mdadm.yml — запуск роли mdadm на k3s_cluster

Инфраструктура:
- inventory: добавлена группа [lab_hosts] с примерами
- host_vars/nas01/vault.yml.example — шаблон credentials для лаб-серверов
- group_vars/all/main.yml: переменные mdadm_enabled и k8s_service_user_*
- Makefile: цели k8s-user и mdadm
- docker/entrypoint.sh: команды k8s-user и mdadm
This commit is contained in:
Sergey Antropoff
2026-04-24 06:50:22 +03:00
parent 24846d2e52
commit 408779a379
15 changed files with 470 additions and 10 deletions

View File

@@ -47,7 +47,7 @@ DOCKER_RUN := docker run --rm -it \
$(IMAGE_NAME)
.PHONY: help setup build rebuild \
bootstrap \
bootstrap k8s-user mdadm \
install install-k3s install-cni install-kubevip install-nfs install-ingress \
install-cert-manager install-istio install-monitoring \
add-node remove-node \
@@ -194,6 +194,17 @@ vault-bootstrap-edit: _check_image ## Редактировать bootstrap vault
$(IMAGE_NAME) \
ansible vault edit host_vars/$(NODE)/vault.yml
k8s-user: _check_env _check_image ## Создать k8s пользователя + разложить SSH ключи на все ноды (cluster + lab_hosts)
@printf "$(CYAN)$(BOLD)Настройка k8s пользователя и SSH ключей...$(NC)\n"
@printf "$(YELLOW)Нужны: host_vars/<node>/vault.yml с bootstrap_user и bootstrap_password для lab_hosts$(NC)\n"
$(DOCKER_RUN) ansible-playbook k8s-user.yml \
$(if $(NODE),-e "node_to_limit=$(NODE)" --limit $(NODE),)
mdadm: _check_env _check_image ## Найти RAID массив и смонтировать в /storage (mdadm_enabled: true)
@printf "$(CYAN)Настройка mdadm RAID...$(NC)\n"
$(DOCKER_RUN) ansible-playbook mdadm.yml \
$(if $(NODE),--limit $(NODE),)
install: _check_env _check_image ## Развернуть полный стек (K3S + kube-vip + NFS + ingress)
@printf "$(CYAN)$(BOLD)Разворачиваю полный K3S стек...$(NC)\n"
$(DOCKER_RUN) install

View File

@@ -59,6 +59,8 @@ print_help() {
echo ""
echo -e "${BOLD}Доступные команды:${NC}"
echo " bootstrap — создать пользователя + задеплоить SSH ключ"
echo " k8s-user — создать k8s пользователя + разложить SSH ключи"
echo " mdadm — найти RAID массив и смонтировать в /storage"
echo " install — полный стек (site.yml)"
echo " install-k3s — только K3S"
echo " install-kubevip — только kube-vip"
@@ -204,6 +206,18 @@ case "${COMMAND}" in
exec ansible-playbook bootstrap.yml "$@"
;;
# ── k8s-user ──────────────────────────────────────────────────────────────
k8s-user)
log "Создание k8s пользователя и деплой SSH ключей..."
run_playbook k8s-user.yml "$@"
;;
# ── mdadm ─────────────────────────────────────────────────────────────────
mdadm)
log "Настройка mdadm RAID и монтирование /storage..."
run_playbook mdadm.yml "$@"
;;
# ── Основные команды ───────────────────────────────────────────────────────
install)
log "Разворачиваю полный K3S стек..."

View File

@@ -49,7 +49,7 @@ k3s_common_packages:
- nfs-common
- mc
- htop
- vim
- nano
- wget
- open-iscsi
- bash-completion
@@ -155,40 +155,54 @@ cert_manager_namespace: "cert-manager"
# none — не создавать (настрой вручную)
# selfsigned — самоподписанный CA (для внутреннего использования)
# letsencrypt — Let's Encrypt (требует публичный домен)
cert_manager_issuer: "selfsigned" # none | selfsigned | letsencrypt
cert_manager_issuer: "letsencrypt" # none | selfsigned | letsencrypt
# Let's Encrypt (нужен только если cert_manager_issuer: letsencrypt)
cert_manager_acme_email: "admin@example.com"
cert_manager_acme_server: "prod" # prod | staging
# ─── etcd backup ──────────────────────────────────────────────────────────────
etcd_backup_dir: "{{ k3s_data_dir }}/server/db/snapshots"
etcd_backup_dir: "{{ k3s_data_dir }}/server/etcd/snapshots"
etcd_backup_retention: 5 # сколько снимков хранить локально
etcd_backup_copy_to_local: false # скопировать снимок на Ansible-хост
etcd_backup_local_dir: "./etcd-backups"
# ─── Istio (Service Mesh) ──────────────────────────────────────────────────────
# istio_enabled: false (задаётся выше)
# istio_version: "1.22.2"
# istio_mtls_mode: "STRICT" # STRICT | PERMISSIVE | DISABLE
# istio_install_gateway: true
# Kiali (UI для Istio) — требует istio_enabled: true
# Токен задаётся в vault.yml:
# vault_kiali_token: "" # заполни после первой установки (токен выведет Ansible)
# kiali_enabled: false (задаётся выше)
# vault_kiali_token: "" # заполни после первой установки (токен выведет Ansible)
# ─── kube-prometheus-stack (Prometheus + Grafana + Alertmanager) ───────────────
# prometheus_stack_enabled: false (задаётся выше)
# prometheus_stack_version: "60.3.0"
# prometheus_retention_days: 7
# prometheus_storage_size: "10Gi"
# Grafana: логин и пароль — задай в vault.yml:
# vault_grafana_user: "admin"
# vault_grafana_password: "ваш-пароль"
# vault_grafana_user: "admin"
# vault_grafana_password: "ваш-пароль"
# grafana_admin_user: "{{ vault_grafana_user | default('admin') }}"
# prometheus_grafana_admin_password: "{{ vault_grafana_password | default('admin') }}"
# prometheus_grafana_ingress_enabled: false
# prometheus_grafana_ingress_host: "grafana.local"
# ─── mdadm ────────────────────────────────────────────────────────────────────
# Поиск RAID массива и монтирование в /storage
# Отключить на конкретной ноде: задай mdadm_enabled: false в host_vars/<node>/main.yml
mdadm_enabled: false
# ─── k8s-user ─────────────────────────────────────────────────────────────────
# Сервисный пользователь k8s — создаётся на всех серверах (кластер + lab_hosts)
# RSA 4096 ключ генерируется один раз на первом мастере, затем раскладывается везде
k8s_service_user: k8s
k8s_service_user_shell: /bin/bash
k8s_service_user_comment: "K8S Service Account"
k8s_service_user_key_type: rsa
k8s_service_user_key_bits: 4096
k8s_service_user_key_comment: "k8s@cluster"
k8s_service_user_ssh_dir: ".ssh"
k8s_service_user_sudo: true

View File

@@ -0,0 +1,18 @@
# ─────────────────────────────────────────────────────────────────────────────
# Bootstrap credentials для лабораторного сервера nas01
# Используются при ansible-playbook k8s-user.yml для создания k8s пользователя
# и деплоя SSH ключей через пароль (без предварительного SSH ключа)
#
# Для создания зашифрованного файла:
# cp host_vars/nas01/vault.yml.example host_vars/nas01/vault.yml
# ansible-vault encrypt host_vars/nas01/vault.yml
# ─────────────────────────────────────────────────────────────────────────────
# Начальный пользователь на сервере (root, admin, ubuntu, и т.д.)
bootstrap_user: root
# SSH пароль этого пользователя
bootstrap_password: "замени-на-реальный-пароль"
# Sudo пароль (если отличается от bootstrap_password, иначе можно не указывать)
# bootstrap_sudo_password: "другой-пароль"

View File

@@ -30,3 +30,13 @@ master01
# Если NFS на отдельной машине — закомментируй строку выше и раскомментируй:
# nfshost ansible_host=192.168.1.20 ansible_user=ubuntu
# ─────────────────────────────────────────────────────────────────────────────
# Лабораторные серверы (не входят в k3s кластер)
# Используются для: распределения SSH ключей k8s пользователя, /etc/hosts
# Подключение: через bootstrap_user/bootstrap_password из host_vars/<host>/vault.yml
# ─────────────────────────────────────────────────────────────────────────────
[lab_hosts]
# nas01 ansible_host=192.168.1.30
# devbox ansible_host=192.168.1.31
# proxy01 ansible_host=192.168.1.32

73
k8s-user.yml Normal file
View File

@@ -0,0 +1,73 @@
---
# ─────────────────────────────────────────────────────────────────────────────
# k8s-user: создание сервисного пользователя k8s на всех серверах
#
# Последовательность:
# 1. Создать пользователя k8s + sudo на всех нодах кластера
# 2. Сгенерировать RSA 4096 ключевую пару на первом мастере (один раз)
# 3. Разложить ключи на все ноды кластера (SSH в любую сторону)
# 4. Обновить /etc/hosts на нодах кластера
# 5. То же самое для lab_hosts (через пароль из vault)
#
# Запуск: ansible-playbook k8s-user.yml --ask-vault-pass
# Только кластер: ansible-playbook k8s-user.yml --limit k3s_cluster
# ─────────────────────────────────────────────────────────────────────────────
# ── 1. Создать пользователя k8s на всех нодах кластера ───────────────────────
- name: Create k8s service user on cluster nodes
hosts: k3s_cluster
gather_facts: true
become: true
roles:
- role: k8s-user
# ── 2. Сгенерировать ключевую пару на первом мастере ─────────────────────────
- name: Generate k8s SSH key pair (first master only)
hosts: "{{ groups['k3s_master'][0] }}"
gather_facts: false
become: true
tasks:
- name: Generate RSA key pair and store facts
ansible.builtin.include_tasks: roles/k8s-user/tasks/generate_keys.yml
# ── 3. Разложить ключи на все ноды кластера ──────────────────────────────────
- name: Distribute k8s SSH keys to all cluster nodes
hosts: k3s_cluster
gather_facts: false
become: true
tasks:
- name: Deploy keys to node
ansible.builtin.include_tasks: roles/k8s-user/tasks/distribute_keys.yml
# ── 4. Обновить /etc/hosts на нодах кластера ─────────────────────────────────
- name: Update /etc/hosts on cluster nodes
hosts: k3s_cluster
gather_facts: false
become: true
tasks:
- name: Update hosts file
ansible.builtin.include_tasks: roles/k8s-user/tasks/update_hosts.yml
# ── 5. Bootstrap lab_hosts: создать пользователя, разложить ключи, обновить hosts
# Подключение через логин/пароль из host_vars/<host>/vault.yml
- name: Setup k8s user on lab hosts
hosts: lab_hosts
gather_facts: true
become: true
vars:
ansible_user: "{{ bootstrap_user }}"
ansible_password: "{{ bootstrap_password }}"
ansible_become_password: "{{ bootstrap_sudo_password | default(bootstrap_password) }}"
ansible_ssh_common_args: >-
-o StrictHostKeyChecking=no
-o PasswordAuthentication=yes
-o PubkeyAuthentication=no
tasks:
- name: Create k8s user on lab host
ansible.builtin.include_tasks: roles/k8s-user/tasks/create_user.yml
- name: Distribute k8s SSH keys to lab host
ansible.builtin.include_tasks: roles/k8s-user/tasks/distribute_keys.yml
- name: Update /etc/hosts on lab host
ansible.builtin.include_tasks: roles/k8s-user/tasks/update_hosts.yml

15
mdadm.yml Normal file
View File

@@ -0,0 +1,15 @@
---
# ─────────────────────────────────────────────────────────────────────────────
# mdadm: обнаружение RAID массива и монтирование в /storage
#
# Запуск: ansible-playbook mdadm.yml --ask-vault-pass
# Только одна нода: ansible-playbook mdadm.yml --limit master01
# Отключить: задай mdadm_enabled: false в host_vars или group_vars
# ─────────────────────────────────────────────────────────────────────────────
- name: Configure mdadm RAID and mount /storage
hosts: k3s_cluster
gather_facts: true
become: true
roles:
- role: mdadm

View File

@@ -0,0 +1,24 @@
---
# ─── k8s-user — сервисный пользователь для управления кластером ──────────────
# Имя пользователя
k8s_service_user: k8s
# Shell
k8s_service_user_shell: /bin/bash
# Комментарий
k8s_service_user_comment: "K8S Service Account"
# Тип SSH ключа и длина
k8s_service_user_key_type: rsa
k8s_service_user_key_bits: 4096
# Комментарий в публичном ключе
k8s_service_user_key_comment: "k8s@cluster"
# Путь к ключам на серверах (внутри home пользователя)
k8s_service_user_ssh_dir: ".ssh"
# Разрешить sudo без пароля для k8s пользователя
k8s_service_user_sudo: true

View File

@@ -0,0 +1,36 @@
---
# Создание пользователя k8s + sudo на текущем хосте
- name: Create k8s user group
ansible.builtin.group:
name: "{{ k8s_service_user }}"
state: present
become: true
- name: Create k8s service user
ansible.builtin.user:
name: "{{ k8s_service_user }}"
comment: "{{ k8s_service_user_comment }}"
shell: "{{ k8s_service_user_shell }}"
group: "{{ k8s_service_user }}"
create_home: true
state: present
become: true
- name: Ensure .ssh directory exists for k8s user
ansible.builtin.file:
path: "/home/{{ k8s_service_user }}/{{ k8s_service_user_ssh_dir }}"
state: directory
owner: "{{ k8s_service_user }}"
group: "{{ k8s_service_user }}"
mode: '0700'
become: true
- name: Grant passwordless sudo to k8s user
ansible.builtin.copy:
dest: "/etc/sudoers.d/{{ k8s_service_user }}"
content: "{{ k8s_service_user }} ALL=(ALL) NOPASSWD:ALL\n"
mode: '0440'
validate: visudo -cf %s
become: true
when: k8s_service_user_sudo | bool

View File

@@ -0,0 +1,28 @@
---
# Раскладывает приватный и публичный ключ k8s пользователя на текущий хост
# Ключи берутся из hostvars первого мастера (сгенерированы там play'ем generate_keys)
- name: Deploy private key to k8s user
ansible.builtin.copy:
content: "{{ hostvars[groups['k3s_master'][0]]['k8s_ssh_private_key'] }}"
dest: "/home/{{ k8s_service_user }}/{{ k8s_service_user_ssh_dir }}/id_rsa"
owner: "{{ k8s_service_user }}"
group: "{{ k8s_service_user }}"
mode: '0600'
become: true
- name: Deploy public key to k8s user
ansible.builtin.copy:
content: "{{ hostvars[groups['k3s_master'][0]]['k8s_ssh_public_key'] }}\n"
dest: "/home/{{ k8s_service_user }}/{{ k8s_service_user_ssh_dir }}/id_rsa.pub"
owner: "{{ k8s_service_user }}"
group: "{{ k8s_service_user }}"
mode: '0644'
become: true
- name: Add k8s public key to authorized_keys
ansible.posix.authorized_key:
user: "{{ k8s_service_user }}"
key: "{{ hostvars[groups['k3s_master'][0]]['k8s_ssh_public_key'] }}"
state: present
become: true

View File

@@ -0,0 +1,50 @@
---
# Генерация RSA 4096 ключевой пары для k8s пользователя
# Выполняется ОДИН РАЗ на первом мастере, ключи затем распространяются на все хосты
- name: Generate RSA {{ k8s_service_user_key_bits }} key pair for k8s user
ansible.builtin.command:
cmd: >
ssh-keygen
-t {{ k8s_service_user_key_type }}
-b {{ k8s_service_user_key_bits }}
-N ''
-C "{{ k8s_service_user_key_comment }}"
-f /home/{{ k8s_service_user }}/{{ k8s_service_user_ssh_dir }}/id_rsa
creates: "/home/{{ k8s_service_user }}/{{ k8s_service_user_ssh_dir }}/id_rsa"
become: true
become_user: "{{ k8s_service_user }}"
- name: Set correct permissions on private key
ansible.builtin.file:
path: "/home/{{ k8s_service_user }}/{{ k8s_service_user_ssh_dir }}/id_rsa"
owner: "{{ k8s_service_user }}"
group: "{{ k8s_service_user }}"
mode: '0600'
become: true
- name: Set correct permissions on public key
ansible.builtin.file:
path: "/home/{{ k8s_service_user }}/{{ k8s_service_user_ssh_dir }}/id_rsa.pub"
owner: "{{ k8s_service_user }}"
group: "{{ k8s_service_user }}"
mode: '0644'
become: true
- name: Slurp private key from first master
ansible.builtin.slurp:
src: "/home/{{ k8s_service_user }}/{{ k8s_service_user_ssh_dir }}/id_rsa"
register: k8s_private_key_raw
become: true
- name: Slurp public key from first master
ansible.builtin.slurp:
src: "/home/{{ k8s_service_user }}/{{ k8s_service_user_ssh_dir }}/id_rsa.pub"
register: k8s_public_key_raw
become: true
- name: Store key content as persistent facts (доступны во всех последующих plays)
ansible.builtin.set_fact:
k8s_ssh_private_key: "{{ k8s_private_key_raw.content | b64decode }}"
k8s_ssh_public_key: "{{ k8s_public_key_raw.content | b64decode | trim }}"
cacheable: true

View File

@@ -0,0 +1,3 @@
---
- name: Create k8s service user
ansible.builtin.include_tasks: create_user.yml

View File

@@ -0,0 +1,23 @@
---
# Обновляет /etc/hosts: добавляет все ноды кластера и лабораторные серверы
- name: Add k3s cluster nodes to /etc/hosts
ansible.builtin.blockinfile:
path: /etc/hosts
marker: "# {mark} ANSIBLE MANAGED - K3S CLUSTER"
block: |
{% for host in groups['k3s_cluster'] %}
{{ hostvars[host]['ansible_host'] }} {{ host }}
{% endfor %}
become: true
- name: Add lab hosts to /etc/hosts
ansible.builtin.blockinfile:
path: /etc/hosts
marker: "# {mark} ANSIBLE MANAGED - LAB HOSTS"
block: |
{% for host in groups['lab_hosts'] %}
{{ hostvars[host]['ansible_host'] }} {{ host }}
{% endfor %}
become: true
when: groups['lab_hosts'] | default([]) | length > 0

View File

@@ -0,0 +1,23 @@
---
# ─── mdadm — поиск RAID и монтирование в /storage ────────────────────────────
# Включить установку и монтирование mdadm RAID
mdadm_enabled: true
# Точка монтирования для найденного RAID массива
mdadm_mount_point: /storage
# Опции монтирования в fstab
# nofail — не останавливать загрузку если RAID не найден
mdadm_mount_opts: "defaults,nofail,x-systemd.device-timeout=5"
# Файловая система на RAID (auto — определяется автоматически)
mdadm_fstype: auto
# Права на точку монтирования
mdadm_mount_point_mode: "0755"
mdadm_mount_point_owner: root
mdadm_mount_point_group: root
# Обновить конфиг mdadm.conf после обнаружения массива
mdadm_update_config: true

118
roles/mdadm/tasks/main.yml Normal file
View File

@@ -0,0 +1,118 @@
---
- name: Skip mdadm if not enabled
ansible.builtin.meta: end_play
when: not mdadm_enabled | default(true) | bool
- name: Install mdadm package
ansible.builtin.apt:
name: mdadm
state: present
update_cache: false
become: true
- name: Scan for active RAID arrays
ansible.builtin.command: mdadm --detail --scan
register: mdadm_scan
changed_when: false
failed_when: false
become: true
- name: Show discovered RAID arrays
ansible.builtin.debug:
msg: "{{ mdadm_scan.stdout_lines }}"
when: mdadm_scan.stdout | length > 0
- name: Warn if no RAID arrays found
ansible.builtin.debug:
msg: "Внимание: RAID массивы не найдены на {{ inventory_hostname }}. Пропускаем монтирование."
when: mdadm_scan.stdout | length == 0 or mdadm_scan.rc != 0
- name: Extract first RAID device name
ansible.builtin.set_fact:
mdadm_device: >-
{{ mdadm_scan.stdout
| regex_search('ARRAY\s+(/dev/md[^\s]+)', '\1')
| first }}
when:
- mdadm_scan.rc == 0
- mdadm_scan.stdout | regex_search('ARRAY\s+/dev/md')
- name: Check RAID device is accessible
ansible.builtin.stat:
path: "{{ mdadm_device }}"
register: mdadm_device_stat
become: true
when: mdadm_device is defined
- name: Get UUID of RAID device (for stable fstab entry)
ansible.builtin.command: blkid -s UUID -o value {{ mdadm_device }}
register: mdadm_uuid
changed_when: false
become: true
when:
- mdadm_device is defined
- mdadm_device_stat.stat.exists | default(false)
- name: Detect filesystem type on RAID device
ansible.builtin.command: blkid -s TYPE -o value {{ mdadm_device }}
register: mdadm_detected_fstype
changed_when: false
become: true
when:
- mdadm_device is defined
- mdadm_device_stat.stat.exists | default(false)
- mdadm_fstype == "auto"
- name: Set filesystem type fact
ansible.builtin.set_fact:
mdadm_real_fstype: >-
{{ mdadm_detected_fstype.stdout | default('ext4')
if mdadm_fstype == 'auto'
else mdadm_fstype }}
when: mdadm_device is defined
- name: Create mount point directory
ansible.builtin.file:
path: "{{ mdadm_mount_point }}"
state: directory
mode: "{{ mdadm_mount_point_mode }}"
owner: "{{ mdadm_mount_point_owner }}"
group: "{{ mdadm_mount_point_group }}"
become: true
when: mdadm_device is defined
- name: Mount RAID array via fstab (UUID-based, persistent)
ansible.posix.mount:
path: "{{ mdadm_mount_point }}"
src: "UUID={{ mdadm_uuid.stdout | trim }}"
fstype: "{{ mdadm_real_fstype }}"
opts: "{{ mdadm_mount_opts }}"
state: mounted
become: true
when:
- mdadm_device is defined
- mdadm_uuid.stdout | default('') | length > 0
- name: Update mdadm.conf with discovered arrays
ansible.builtin.shell: |
mdadm --detail --scan >> /etc/mdadm/mdadm.conf
update-initramfs -u 2>/dev/null || true
args:
executable: /bin/bash
become: true
changed_when: true
when:
- mdadm_update_config | bool
- mdadm_device is defined
- name: Show mount status
ansible.builtin.command: df -h {{ mdadm_mount_point }}
register: mount_status
changed_when: false
become: true
when: mdadm_device is defined
- name: Display RAID mount result
ansible.builtin.debug:
msg: "{{ mount_status.stdout_lines }}"
when: mdadm_device is defined