← Назад к Блогу

Kubernetes за Минимальный Бюджет: Rancher + RKE2 Кластер из 3 Нод на Hetzner Cloud

Опубликовано · 16 мин чтения

  • rancher
  • rke2
  • hetzner
  • kubernetes
  • docker
  • nfs
  • ansible
  • terraform

Полная настройка Kubernetes-кластера с Rancher, RKE2, NFS-хранилищем и Load Balancer на Hetzner Cloud -- все автоматизировано с помощью Terraform и Ansible примерно за 25 EUR/месяц.

TL;DR

Настраиваем полностью рабочий Kubernetes-кластер на Hetzner Cloud общей стоимостью ~25 EUR/месяц: Rancher в качестве UI для управления (Docker-контейнер), 3 ноды RKE2 с ролями etcd + controlplane + worker, NFS-хранилище на Hetzner Volume и Load Balancer -- все автоматизировано с помощью Terraform и Ansible. В завершение устанавливаем NFS Subdir External Provisioner для динамических персистентных томов.


Почему Именно Такой Setup?

Если вы стартап, фрилансер или небольшая команда, которой нужен Kubernetes, но вы не хотите платить 200+ EUR/месяц за EKS/GKE, Hetzner Cloud -- один из лучших вариантов по соотношению цена-производительность в Европе.

Что мы получим:

  • Rancher v2.13.2 -- полноценный UI для управления кластером
  • RKE2 с Kubernetes v1.34.2 -- дистрибутив с акцентом на безопасность, CIS compliant
  • Calico CNI -- продвинутые сетевые политики
  • 3 ноды all-in-one (etcd + controlplane + worker)
  • NFS-хранилище на Hetzner Volume (100GB) для динамических PersistentVolumes
  • Hetzner Load Balancer для HTTP/HTTPS-трафика
  • Полная автоматизация через Terraform + Ansible -- 100% воспроизводимость

Почему мы не использовали Helm для Rancher? В продакшн-окружении с HA Rancher устанавливается через Helm на выделенный RKE2-кластер. Но для окружения разработки или небольшого кластера один Docker-контейнер проще, быстрее перезапускается и потребляет меньше ресурсов. Rancher в Docker создает и провиженит RKE2-кластер на отдельных нодах, поэтому разделение management-workload сохраняется.


Архитектура

+-----------------------------------------------------------------+
|                    Hetzner Cloud - nbg1 (Nuremberg)              |
|                    VPC: 10.10.0.0/16                             |
|                                                                  |
|                        +-----------------------------+           |
|                        |    Load Balancer (lb11)      |          |
|                        |    10.10.0.99                |          |
|                        |    Ports: 80, 443            |          |
|                        +--------------+---------------+          |
|                                       |                          |
|  +---------------+     +--------------------------------------+  |
|  |   Rancher     |     |      RKE2 Cluster (Calico CNI)      |   |
|  |   (Docker)    |     |                                     |   |
|  |   cx32        |---->|  +--------+ +--------+ +--------+   |   |
|  |  10.10.0.100  |     |  | k8s-1  | | k8s-2  | | k8s-3  |   |   |
|  |  Ubuntu 24.04 |     |  |  cpx32 | |  cpx32 | |  cpx32 |   |   |
|  +--------------+      |  |  .11   | |  .12   | |  .13   |   |   |
|                        |  | etcd   | | etcd   | | etcd   |   |   |
|  +--------------+      |  | ctrl   | | ctrl   | | ctrl   |   |   |
|  |     NFS       |     |  | worker | | worker | | worker |   |   |
|  |   Server      |     |  +----+---+ +----+---+ +----+---+   |   |
|  |   cx22        |     |       |          |          |       |   |
|  |  10.10.0.75   |<----|-------+----------+----------+       |   |
|  |  +100GB Vol   |     |                                     |   |
|  +--------------+     +--------------------------------------+   |
+-----------------------------------------------------------------+

Каждая K8s-нода несет все 3 роли:

  • etcd -- распределенная база данных кластера (кворум из 3)
  • controlplane -- API server, scheduler, controller manager
  • worker -- выполняет рабочие нагрузки приложений

Эта модель all-in-one снижает затраты за счет отказа от выделенных нод, сохраняя при этом отказоустойчивость etcd с кворумом из 3 нод.


Стоимость Hetzner Cloud (Февраль 2026, EU)

| Ресурс         | Тип   | Характеристики            | Цена/месяц              |
|----------------|-------|---------------------------|-------------------------|
| Rancher Server | CX32  | 4 vCPU, 8GB RAM, 80GB SSD | 6.80 EUR                |
| K8s Node x3    | CPX32 | 4 vCPU AMD, 8GB RAM, 80GB | 35.70 EUR (3x11.90 EUR) |
| NFS Server     | CX22  | 2 vCPU, 4GB RAM, 40GB SSD | 3.79 EUR                |
| Hetzner Volume | 100GB | NFS storage ext4          | 4.80 EUR                |
| Load Balancer  | LB11  | Round Robin, 80/443       | 6.49 EUR                |
| **ИТОГО**      |       |                           | **~57.58 EUR/месяц**    |

Примечание: Цены указаны без НДС для локации Германия. С оптимизациями (CX22 для Rancher, ноды поменьше) можно уложиться в ~25-30 EUR/месяц для минимального кластера. Все серверы включают 20TB трафика/месяц.


Фаза 0: Подготовка Окружения

Требования

# Terraform >= 1.0
terraform version

# Ansible
ansible --version

# Hetzner CLI (опционально, но полезно)
hcloud version

# jq (необходим на Rancher server для init-скрипта)
jq --version

Структура Проекта

infrastructure/
+-- terraform/
|   +-- 01-variables.tf      # Переменные с валидацией
|   +-- 02-locals.tf         # Общие теги
|   +-- 03-providers.tf      # Hetzner provider
|   +-- 04-network.tf        # VPC + Subnet
|   +-- 05-security.tf       # SSH-ключи + базовый Firewall
|   +-- 06-storage.tf        # NFS Volume 100GB
|   +-- 07-compute.tf        # Все серверы + Placement Group
|   +-- 08-load-balancer.tf  # LB для K8s
|   +-- 09-security-whitelist.tf  # Firewall с IP-адресами серверов
|   +-- 90-outputs.tf        # Выходные IP-адреса
|
+-- ansible/
    +-- rancher.yaml          # Основной playbook
    +-- nfs1.yaml     # NFS playbook
    +-- inventory/
    |   +-- hosts.ini
    +-- roles/
        +-- common/           # Обновление, утилиты
        +-- users/            # SSH-пользователи
        +-- docker/           # Установка Docker
        +-- nfs/
        |   +-- tasks/main.yml
        |   +-- defaults/main.yml
        |   +-- handlers/main.yml
        |   +-- templates/exports.j2
        +-- rancher/
            +-- tasks/main.yml
            +-- defaults/main.yml
            +-- templates/init_rancher.sh.j2

Фаза 1: Terraform -- Провижининг Инфраструктуры

1.1 Переменные

Определяем все переменные с валидацией. Обратите внимание на map instance_type, который централизует размеры серверов -- идеально, когда нужно быстро переключиться между dev (cx22) и production (cpx32):

# 01-variables.tf

variable "hcloud_token" {
  description = "Hetzner Cloud API token"
  sensitive   = true
  type        = string
}

variable "location" {
  default     = "nbg1"
  description = "Hetzner Cloud datacenter location"
  type        = string
  validation {
    condition     = contains(["nbg1", "fsn1", "hel1", "ash"], var.location)
    error_message = "Location must be one of: nbg1, fsn1, hel1, ash."
  }
}

variable "os_type" {
  default     = "debian-12"
  description = "Operating system image for the server"
  type        = string
}

variable "env" {
  default     = "dev"
  description = "Environment name"
  type        = string
}

variable "is_production" {
  description = "Whether this is production environment or dev"
  type        = bool
  default     = false
}

variable "project" {
  default     = "mit"
  description = "Name of project"
  type        = string
}

variable "user_ssh_keys" {
  description = "Map of username to SSH public key content"
  type        = map(string)
  default = {
    "mcujba" = "ssh-ed25519 AAAAC3Nza... maxim"
  }
}

variable "whitelist" {
  default     = [
    "188.0.222.69/32",
    "178.169.11.99/32",
  ]
  description = "Whitelisted IP addresses for firewall access"
  type        = list(string)
}

variable "k8s_count" {
  default     = 3
  description = "Number of Kubernetes worker nodes"
  type        = number
  validation {
    condition     = var.k8s_count >= 1 && var.k8s_count <= 10
    error_message = "Kubernetes worker count must be between 1 and 10."
  }
}

variable "instance_type" {
  default = {
    "k8s"     = "cpx32"
    "rancher" = "cx32"
    "nfs"     = "cx22"
  }
  type        = map(string)
  description = "Instance types for different server roles"
}

variable "lb_services" {
  default     = [80, 443]
  description = "Ports for load balancer services"
  type        = list(number)
}

variable "vpc_cidr" {
  default     = "10.10.0.0/16"
  description = "VPC CIDR for environment"
  type        = string
}

1.2 Сеть -- VPC и Subnet

Все серверы обмениваются данными через частную сеть. Внутренний трафик не учитывается в лимите пропускной способности:

# 04-network.tf

resource "hcloud_network" "vpc" {
  name     = "vpc-${var.project}-${var.env}"
  ip_range = var.vpc_cidr
}

resource "hcloud_network_subnet" "servers" {
  network_id   = hcloud_network.vpc.id
  type         = "cloud"
  network_zone = "eu-central"
  ip_range     = cidrsubnet(var.vpc_cidr, 4, 0)
}

1.3 Безопасность -- SSH-ключи и Firewall

Используем стратегию двойного файрвола: базовый файрвол (создается до серверов) и второй, включающий IP-адреса серверов (создается после compute). Это позволяет избежать циклической зависимости:

# 05-security.tf

resource "hcloud_ssh_key" "users" {
  for_each   = var.user_ssh_keys
  name       = "ssh-key-${var.project}-${var.env}-${each.key}"
  public_key = each.value
}

resource "hcloud_firewall" "in_filter" {
  name = "firewall-${var.project}-${var.env}"

  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "any"
    source_ips = concat([var.vpc_cidr], var.whitelist)
  }
  rule {
    direction  = "in"
    protocol   = "icmp"
    source_ips = concat([var.vpc_cidr], var.whitelist)
  }
}

resource "hcloud_firewall_attachment" "servers" {
  firewall_id     = hcloud_firewall.in_filter.id
  label_selectors = ["Project=${var.project}"]
}

Второй файрвол, включающий IP-адреса серверов:

# 09-security-whitelist.tf

locals {
  server_whitelisted_ips = concat(
    formatlist("%s/32", hcloud_server.k8s.*.ipv4_address),
    [format("%s/32", hcloud_server.rancher.ipv4_address)],
    [var.vpc_cidr],
    var.whitelist
  )
}

resource "hcloud_firewall" "server_access" {
  name = "server-access-${var.project}-${var.env}"

  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "any"
    source_ips = local.server_whitelisted_ips
  }
  rule {
    direction  = "in"
    protocol   = "udp"
    port       = "any"
    source_ips = local.server_whitelisted_ips
  }
  rule {
    direction  = "in"
    protocol   = "icmp"
    source_ips = local.server_whitelisted_ips
  }

  depends_on = [
    hcloud_server.k8s,
    hcloud_server.rancher,
    hcloud_server.nfs
  ]
}

1.4 Хранилище -- Hetzner Volume для NFS

Hetzner Volumes -- это персистентные блочные хранилища. Даже при пересоздании NFS-сервера данные остаются нетронутыми:

# 06-storage.tf

resource "hcloud_volume" "nfs" {
  name              = "nfs1-${var.project}-${var.env}-volume"
  size              = 100
  location          = var.location
  format            = "ext4"
  delete_protection = var.is_production ? true : false
}

resource "hcloud_volume_attachment" "nfs" {
  volume_id  = hcloud_volume.nfs.id
  server_id  = hcloud_server.nfs.id
  automount  = true
  depends_on = [hcloud_volume.nfs]
}

1.5 Вычислительные Ресурсы -- Серверы

Ключевой момент: K8s-ноды используют Placement Group типа spread. Hetzner гарантирует, что серверы из группы размещаются на разных физических хостах -- критически важно для high availability:

# 07-compute.tf

resource "hcloud_placement_group" "k8s" {
  name = "k8s-${var.project}-${var.env}"
  type = "spread"
}

resource "hcloud_server" "k8s" {
  count              = var.k8s_count
  name               = "k8s${count.index + 1}-${var.project}-${var.env}"
  image              = "ubuntu-24.04"
  server_type        = lookup(var.instance_type, "k8s", "cpx32")
  location           = var.location
  ssh_keys           = [for key in hcloud_ssh_key.users : key.id]
  placement_group_id = hcloud_placement_group.k8s.id

  public_net {
    ipv4_enabled = true
    ipv6_enabled = false
  }
  network {
    network_id = hcloud_network.vpc.id
    ip         = cidrhost(hcloud_network_subnet.servers.ip_range,
                          count.index + 11)
  }

  labels = {
    Terraform   = "true"
    Environment = var.env
    Project     = var.project
    type        = "Kubernetes"
  }

  depends_on = [
    hcloud_network_subnet.servers,
    hcloud_placement_group.k8s,
    hcloud_ssh_key.users
  ]

  lifecycle {
    ignore_changes = [ssh_keys]
  }
}

resource "hcloud_server" "rancher" {
  name        = "rancher1-${var.project}-${var.env}"
  image       = "ubuntu-24.04"
  server_type = lookup(var.instance_type, "rancher", "cx32")
  location    = var.location
  ssh_keys    = [for key in hcloud_ssh_key.users : key.id]
  backups     = true

  public_net {
    ipv4_enabled = true
    ipv6_enabled = false
  }
  network {
    network_id = hcloud_network.vpc.id
    ip         = cidrhost(var.vpc_cidr, 100)
  }

  labels = {
    Terraform   = "true"
    Environment = var.env
    Project     = var.project
    type        = "Rancher"
  }

  depends_on = [hcloud_network_subnet.servers, hcloud_ssh_key.users]
  lifecycle { ignore_changes = [ssh_keys] }
}

resource "hcloud_server" "nfs" {
  name        = "nfs1-${var.project}-${var.env}"
  image       = var.os_type
  server_type = lookup(var.instance_type, "nfs", "cx22")
  location    = var.location
  ssh_keys    = [for key in hcloud_ssh_key.users : key.id]
  backups     = true

  public_net {
    ipv4_enabled = true
    ipv6_enabled = false
  }
  network {
    network_id = hcloud_network.vpc.id
    ip         = cidrhost(var.vpc_cidr, 75)
  }

  labels = {
    Terraform   = "true"
    Environment = var.env
    Project     = var.project
    type        = "NFS"
  }

  depends_on = [hcloud_network_subnet.servers, hcloud_volume.nfs]
  lifecycle { ignore_changes = [ssh_keys] }
}

1.6 Load Balancer

Hetzner Load Balancer направляет трафик через частную сеть (use_private_ip = true) к K8s-нодам:

# 08-load-balancer.tf

resource "hcloud_load_balancer" "k8s" {
  name               = "k8s-lb-${var.project}-${var.env}"
  load_balancer_type = "lb11"
  location           = var.location
  algorithm { type = "round_robin" }
}

resource "hcloud_load_balancer_target" "k8s" {
  count            = var.k8s_count
  type             = "server"
  load_balancer_id = hcloud_load_balancer.k8s.id
  server_id        = hcloud_server.k8s[count.index].id
  use_private_ip   = true
}

resource "hcloud_load_balancer_service" "k8s" {
  count            = length(var.lb_services)
  load_balancer_id = hcloud_load_balancer.k8s.id
  protocol         = "tcp"
  listen_port      = var.lb_services[count.index]
  destination_port = var.lb_services[count.index]
}

resource "hcloud_load_balancer_network" "k8s" {
  load_balancer_id        = hcloud_load_balancer.k8s.id
  subnet_id               = hcloud_network_subnet.servers.id
  enable_public_interface = true
  ip                      = "10.10.0.99"
}

1.7 Развертывание

export TF_VAR_hcloud_token="your-hetzner-api-token"

cd infrastructure/terraform
terraform init
terraform plan
terraform apply

Фаза 2: Ansible -- Установка NFS-сервера

2.1 Inventory

# ansible/inventory/hosts.ini

[rancher]
rancher1  ansible_host=49.12.xxx.xxx

[kubernetes]
k8s1  ansible_host=49.12.aaa.aaa  ansible_all_ipv4_addresses='["49.12.aaa.aaa","10.10.0.11"]'
k8s2  ansible_host=49.12.bbb.bbb  ansible_all_ipv4_addresses='["49.12.bbb.bbb","10.10.0.12"]'
k8s3  ansible_host=49.12.ccc.ccc  ansible_all_ipv4_addresses='["49.12.ccc.ccc","10.10.0.13"]'

[nfs]
nfs1  ansible_host=49.12.yyy.yyy

[all:vars]
ansible_user=root
ansible_python_interpreter=/usr/bin/python3

2.2 Роль NFS

NFS-сервер автоматически обнаруживает подключенный Hetzner Volume (по паттерну HC_Volume_*) и экспортирует директорию для K8s-нод через частную сеть:

# roles/nfs/tasks/main.yml

- name: Ensure NFS utilities are installed
  apt:
    name:
      - nfs-kernel-server
    state: present
    update_cache: true

- name: Identify Hetzner mounted volume
  set_fact:
    mount_volume: "{{ ansible_mounts | json_query(query) }}"
  vars:
    query: "[?contains(mount, `/mnt/HC_Volume_`)].mount"

- name: Volume is mounted to
  debug:
    var: mount_volume

- name: Ensure NFS is running
  systemd_service:
    name: nfs-server
    state: started
    enabled: true

- name: Copy exports file
  template:
    src: exports.j2
    dest: /etc/exports
    owner: root
    group: root
    mode: 0644
  notify: reload nfs

Шаблон exports -- автоматически генерирует запись экспорта для каждой K8s-ноды:

# roles/nfs/templates/exports.j2
# Do not edit manually - generated by Ansible

{% raw %}
{% for host in groups[mount_hosts] %}
{{ mount_volume | first }} {{ hostvars[host].ansible_all_ipv4_addresses | ansible.utils.ipaddr(app_cidr) | first }}(rw,sync,no_root_squash,no_all_squash,no_subtree_check)
{% endfor %}
{% endraw %}

В результате файл /etc/exports будет выглядеть так:

/mnt/HC_Volume_12345678 10.10.0.11(rw,sync,no_root_squash,no_all_squash,no_subtree_check)
/mnt/HC_Volume_12345678 10.10.0.12(rw,sync,no_root_squash,no_all_squash,no_subtree_check)
/mnt/HC_Volume_12345678 10.10.0.13(rw,sync,no_root_squash,no_all_squash,no_subtree_check)

Фильтр ansible.utils.ipaddr(app_cidr) выбирает только IP-адрес, принадлежащий частной сети 10.10.0.0/16, гарантируя, что NFS-трафик никогда не выйдет через публичный интерфейс.

Переменные и handler:

# roles/nfs/defaults/main.yml
mount_hosts: kubernetes
app_cidr: 10.10.0.0/16

# roles/nfs/handlers/main.yml
- name: reload nfs
  command: 'exportfs -ra'

NFS Playbook:

# nfs1.yaml

- name: Get Gather Facts
  gather_facts: yes
  hosts:
    - kubernetes

- name: Install NFS Solution
  become: true
  hosts:
    - nfs
  roles:
    - role: nfs
ansible-playbook -i inventory/hosts.ini nfs1.yaml

Фаза 3: Ansible -- Установка Rancher + Создание Кластера RKE2

Это самая сложная фаза. Playbook выполняет следующее за один запуск:

  1. Устанавливает Docker на сервер Rancher
  2. Устанавливает nfs-common на K8s-ноды (NFS-клиент)
  3. Запускает контейнер Rancher
  4. Выполняет init-скрипт, который: задает пароль, создает API-токен, создает кластер RKE2 через API, генерирует команду join
  5. Выполняет команду join на каждой K8s-ноде с флагами --etcd --controlplane --worker

3.1 Переменные Rancher

# roles/rancher/defaults/main.yml

rancherVersion: v2.13.2
kubernetesVersion: v1.34.2+rke2r3
CNI: calico

3.2 Задачи Rancher

# roles/rancher/tasks/main.yml

- name: Install nfs-common for NFS Client
  package:
    name: "{{ item }}"
  with_items:
    - nfs-common
  when: "'kubernetes' in group_names"

- name: Check if Rancher server is running
  shell: docker ps | grep rancher/rancher
  register: server_running
  ignore_errors: true
  changed_when: false
  when: "'rancher' in group_names"

- name: Wait for Rancher to be ready
  uri:
    url: "https://{{ ansible_default_ipv4.address }}/ping"
    method: GET
    validate_certs: false
    status_code: 200
  register: rancher_health
  until: rancher_health.status == 200
  retries: 30
  delay: 10
  when: "'rancher' in group_names and server_running is success"

- name: Create Rancher log directory
  file:
    path: /var/log/rancher
    state: directory
    recurse: yes
    mode: "0755"
  when: "'rancher' in group_names"

- name: Create Rancher data directory
  file:
    path: /opt/rancher
    state: directory
    mode: "0755"
  when: "'rancher' in group_names"

- name: Start Rancher server
  command: >
    docker run -d
    -e CATTLE_BOOTSTRAP_PASSWORD=admin
    --restart=unless-stopped
    --privileged
    -p 443:443
    -v /var/log/rancher/auditlog:/var/log/auditlog
    -v /opt/rancher:/var/lib/rancher
    rancher/rancher:{{ rancherVersion }}
  when: not server_running is success and "'rancher' in group_names"

- name: Copy Rancher init script
  template:
    src: "init_rancher.sh.j2"
    dest: /tmp/init_rancher.sh
    owner: root
    group: root
    mode: 0777
  when: "'rancher' in group_names"

- name: Run Rancher init script
  shell: bash /tmp/init_rancher.sh
  register: rancher_token
  when: "'rancher' in group_names"

- name: Show registration command
  debug:
    var: rancher_token.stdout
  when: "'rancher' in group_names"

- name: Declare rancher_token fact on K8s hosts
  set_fact:
    rancher_token_fact: "{{ rancher_token.stdout }}"
  delegate_to: "{{ item }}"
  delegate_facts: true
  with_items: "{{ groups['kubernetes'] }}"
  when: "'rancher' in group_names"

- name: Check if Rancher agent is running on nodes
  ansible.builtin.service:
    name: rancher-system-agent
    state: started
  register: masters_agent_running
  ignore_errors: true
  changed_when: false
  when: "'kubernetes' in group_names"

- name: Join nodes to Rancher cluster
  shell: "{{ item }} --etcd --controlplane --worker"
  with_items:
    - "{{ hostvars[inventory_hostname].rancher_token_fact }}"
  when: masters_agent_running is failed and "'kubernetes' in group_names"

3.3 Init-скрипт -- Полная Автоматизация Rancher API

Это сердце автоматизации. Скрипт взаимодействует с Rancher API v3 и v1 для аутентификации, смены пароля, создания API-токена, установки server-url, создания RKE2-кластера и генерации registration token.

Кластер создается следующим API-запросом:

curl -s https://127.0.0.1/v1/provisioning.cattle.io.clusters \
  -X POST \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $LOGINTOKEN" \
  --data-raw '{
    "type": "provisioning.cattle.io.cluster",
    "metadata": {
      "namespace": "fleet-default",
      "name": "dev-cluster"
    },
    "spec": {
      "kubernetesVersion": "v1.32.5+rke2r1",
      "network": {
        "type": "networkConfig",
        "plugin": "calico"
      },
      "localClusterAuthEndpoint": {},
      "rkeConfig": {}
    }
  }' --insecure

3.4 Основной Playbook

# rancher.yaml

- name: Install Docker for Rancher Server only
  become: true
  hosts:
    - rancher
  roles:
    - role: docker

- name: Install Rancher Solution
  become: true
  hosts:
    - rancher
    - kubernetes
  roles:
    - role: rancher

3.5 Запуск

ansible-playbook -i inventory/hosts.ini rancher.yaml

Выполнение этого playbook занимает ~10-15 минут. Контейнеру Rancher требуется ~2-3 минуты на запуск, затем каждой ноде нужно ~3-5 минут для присоединения и bootstrap RKE2.


Фаза 4: Проверка Кластера

4.1 Доступ к Rancher UI

Откройте https://RANCHER_SERVER_IP в браузере. При первом обращении вы увидите предупреждение о самоподписанном сертификате.

4.2 kubectl

# Проверка нод
kubectl get nodes
NAME                    STATUS   ROLES                       AGE   VERSION
k8s1            Ready    control-plane,etcd,worker   10m   v1.32.5+rke2r1
k8s2            Ready    control-plane,etcd,worker   8m    v1.32.5+rke2r1
k8s3            Ready    control-plane,etcd,worker   7m    v1.32.5+rke2r1

# Проверка системных подов
kubectl get pods -A
NAMESPACE         NAME                                      READY   STATUS
kube-system       calico-kube-controllers-xxx               1/1     Running
kube-system       calico-node-xxxxx                         1/1     Running
kube-system       coredns-xxx                               1/1     Running
kube-system       etcd-k8s1                                 1/1     Running
kube-system       kube-apiserver-k8s1                       1/1     Running
kube-system       rke2-ingress-nginx-controller-xxxxx       1/1     Running
kube-system       rke2-metrics-server-xxx                   1/1     Running

Фаза 5: NFS Subdir External Provisioner

Теперь у нас есть работающий кластер, но без динамического хранилища. NFS Subdir External Provisioner автоматически создает поддиректории на NFS-сервере для каждого PersistentVolumeClaim.

5.1 Установка через Helm

helm repo add nfs-subdir-external-provisioner \
  https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm repo update

helm install nfs-provisioner \
  nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
  --namespace kube-system \
  --set nfs.server=10.10.0.75 \
  --set nfs.path=/mnt/HC_Volume_12345678 \
  --set storageClass.name=nfs-client \
  --set storageClass.defaultClass=true \
  --set storageClass.reclaimPolicy=Retain \
  --set storageClass.archiveOnDelete=true

5.2 Проверка

kubectl get sc
NAME                   PROVISIONER                                     RECLAIMPOLICY
nfs-client (default)   cluster.local/nfs-provisioner-nfs-subdir-...    Retain

kubectl get pods -n kube-system | grep nfs
nfs-provisioner-nfs-subdir-external-provisioner-xxx   1/1     Running

5.3 Тест -- Создание PVC

# test-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-nfs-claim
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: test-nfs-pod
spec:
  containers:
    - name: test
      image: busybox
      command: ["sh", "-c", "echo 'NFS works!' > /mnt/data/test.txt && cat /mnt/data/test.txt && sleep 3600"]
      volumeMounts:
        - name: nfs-vol
          mountPath: /mnt/data
  volumes:
    - name: nfs-vol
      persistentVolumeClaim:
        claimName: test-nfs-claim
kubectl apply -f test-pvc.yaml

kubectl get pvc
NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES
test-nfs-claim   Bound    pvc-a1b2c3d4-e5f6-7890-abcd-ef1234567890   1Gi        RWX

kubectl logs test-nfs-pod
NFS works!

# Очистка
kubectl delete -f test-pvc.yaml

Полная Сводка

| Фаза | Инструмент | Что Делает | Время | |------|------------|------------|-------| | 1 | Terraform | Провижинит VPC, серверы, LB, файрвол, том | ~3 мин | | 2 | Ansible | Устанавливает и настраивает NFS-сервер | ~2 мин | | 3 | Ansible | Устанавливает Docker, Rancher, создает кластер, присоединяет ноды | ~15 мин | | 4 | kubectl | Проверка работоспособности кластера | ~1 мин | | 5 | Helm | Устанавливает NFS provisioner для динамических PV | ~1 мин | | Итого | | От нуля до рабочего кластера | ~22 мин |


Советы для Продакшна

  1. Бэкап etcd: Настройте автоматический бэкап etcd -- потеря etcd означает потерю кластера. RKE2 поддерживает автоматические снепшоты в /var/lib/rancher/rke2/server/db/snapshots.

  2. Rancher HA: Для продакшна запускайте Rancher на выделенном RKE2-кластере через Helm, а не в одном Docker-контейнере.

  3. cert-manager: Установите cert-manager + Let's Encrypt для автоматических TLS-сертификатов на RKE2 NGINX ingress.

  4. Мониторинг: RKE2 поставляется с предустановленным metrics-server. Добавьте Prometheus + Grafana из Rancher Marketplace.

  5. Сетевые Политики: Calico поддерживает NetworkPolicies нативно. Определите политики изоляции между namespace-ами с самого начала.

  6. Бэкап Hetzner Volume: Hetzner не делает автоматический бэкап Volumes. Настройте cron job с rsync или периодические снепшоты.

  7. Масштабирование: Чтобы добавить ноды, измените k8s_count в Terraform, выполните apply, обновите inventory Ansible и запустите playbook rancher. Новые ноды присоединятся автоматически.


Ресурсы


Эта статья основана на реальной продакшн-конфигурации. Код Terraform и Ansible протестирован и активно используется для провижининга кластеров разработки.