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

@@ -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