← Înapoi la Blog

Kubernetes pe Budget: Rancher + RKE2 Cluster cu 3 Noduri pe Hetzner Cloud

Publicat · 16 min citire

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

Configurare completa a unui cluster Kubernetes cu Rancher, RKE2, NFS storage si Load Balancer pe Hetzner Cloud - totul automatizat cu Terraform si Ansible pentru ~25 EUR/luna.

TL;DR

Configuram un cluster Kubernetes complet functional pe Hetzner Cloud cu un cost total de ~25 EUR/luna: Rancher ca management UI (Docker container), 3 noduri RKE2 cu rolurile etcd + controlplane + worker, NFS storage cu Hetzner Volume, si un Load Balancer -- totul automatizat cu Terraform si Ansible. La final, instalam NFS Subdir External Provisioner pentru volume persistente dinamice.


De Ce Acest Setup?

Daca esti un startup, un freelancer, sau o echipa mica care are nevoie de Kubernetes dar nu vrea sa plateasca 200+ EUR/luna pentru EKS/GKE, Hetzner Cloud este una dintre cele mai bune optiuni cost-performanta din Europa.

Ce obtinem:

  • Rancher v2.13.2 -- management UI complet pentru cluster
  • RKE2 cu Kubernetes v1.34.2 -- distributie security-focused, CIS compliant
  • Calico CNI -- network policies avansate
  • 3 noduri all-in-one (etcd + controlplane + worker)
  • NFS storage cu Hetzner Volume (100GB) pentru PersistentVolumes dinamice
  • Load Balancer Hetzner pentru trafic HTTP/HTTPS
  • Totul automatizat cu Terraform + Ansible -- reproducibil 100%

De ce nu am folosit Helm pentru Rancher? Intr-un setup de productie HA, Rancher se instaleaza cu Helm pe un cluster RKE2 dedicat. Dar pentru un environment de development sau un cluster mic, un singur container Docker este mai simplu, mai rapid de repornit si consuma mai putine resurse. Rancher in Docker creeaza si provisioneaza cluster-ul RKE2 pe nodurile separate, deci decuplarea management-workload ramane intacta.


Arhitectura

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

Fiecare nod K8s poarta toate cele 3 roluri:

  • etcd -- baza de date distribuita a clusterului (quorum de 3)
  • controlplane -- API server, scheduler, controller manager
  • worker -- ruleaza workload-urile aplicatiilor

Acest model all-in-one reduce costurile prin eliminarea nodurilor dedicate, pastrand in acelasi timp redundanta etcd cu quorum de 3 noduri.


Costuri Hetzner Cloud (Februarie 2026, EU)

| Resursa        | Tip   | Specificatii              | Pret/luna               |
|----------------|-------|---------------------------|-------------------------|
| 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                |
| **TOTAL**      |       |                           | **~57.58 EUR/luna**     |

Nota: Preturile sunt fara TVA, pentru locatia Germania. Cu optimizari (CX22 pentru Rancher, noduri mai mici), poti ajunge la ~25-30 EUR/luna pentru un cluster minimal. Toate serverele includ 20TB trafic/luna.


Faza 0: Pregatirea Mediului

Cerinte

# Terraform >= 1.0
terraform version

# Ansible
ansible --version

# Hetzner CLI (optional dar util)
hcloud version

# jq (necesar pe Rancher server pentru init script)
jq --version

Structura Proiectului

infrastructure/
+-- terraform/
|   +-- 01-variables.tf      # Variabile cu validare
|   +-- 02-locals.tf         # Common tags
|   +-- 03-providers.tf      # Hetzner provider
|   +-- 04-network.tf        # VPC + Subnet
|   +-- 05-security.tf       # SSH keys + Firewall bazic
|   +-- 06-storage.tf        # NFS Volume 100GB
|   +-- 07-compute.tf        # Toate serverele + Placement Group
|   +-- 08-load-balancer.tf  # LB pentru K8s
|   +-- 09-security-whitelist.tf  # Firewall cu server IPs
|   +-- 90-outputs.tf        # Output IPs
|
+-- ansible/
    +-- rancher.yaml          # Playbook principal
    +-- nfs1.yaml     # Playbook NFS
    +-- inventory/
    |   +-- hosts.ini
    +-- roles/
        +-- common/           # Update, tools
        +-- users/            # SSH users
        +-- docker/           # Docker install
        +-- 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

Faza 1: Terraform -- Provisionare Infrastructura

1.1 Variabile

Definim toate variabilele cu validare. Observa instance_type map-ul care centralizeaza dimensionarea serverelor -- ideal cand vrei sa faci rapid switch intre dev (cx22) si productie (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 Networking -- VPC si Subnet

Toate serverele comunica prin reteaua privata. Traficul intern nu contorizeaza la bandwidth:

# 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 Security -- SSH Keys si Firewall

Folosim o strategie de dual-firewall: un firewall bazic (creat inainte de servere) si un al doilea care include IP-urile serverelor (creat dupa compute). Aceasta evita dependenta circulara:

# 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}"]
}

Al doilea firewall care include server IPs:

# 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 Storage -- Hetzner Volume pentru NFS

Hetzner Volumes sunt block storage-uri persistente. Chiar daca serverul NFS este recreat, datele raman intacte:

# 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 Compute -- Serverele

Punctul cheie: K8s nodurile folosesc un Placement Group de tip spread. Hetzner garanteaza ca serverele din grup sunt plasate pe host-uri fizice diferite -- esential pentru 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

Load balancer-ul Hetzner directioneaza traficul prin reteaua privata (use_private_ip = true) catre nodurile 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 Deployment

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

cd infrastructure/terraform
terraform init
terraform plan
terraform apply

Faza 2: Ansible -- Instalare NFS Server

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 Role

NFS server-ul detecteaza automat Hetzner Volume-ul montat (pattern-ul HC_Volume_*) si exporta directorul catre nodurile K8s prin reteaua privata:

# 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

Template exports -- genereaza automat un export per nod 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 %}

Generat, fisierul /etc/exports va arata:

/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)

Filtrul ansible.utils.ipaddr(app_cidr) selecteaza doar adresa IP care apartine retelei private 10.10.0.0/16, asigurand ca traficul NFS nu iese niciodata pe interfata publica.

Variabile si 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'

Playbook NFS:

# 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

Faza 3: Ansible -- Instalare Rancher + Crearea Clusterului RKE2

Aceasta este faza cea mai complexa. Playbook-ul face urmatoarele intr-o singura rulare:

  1. Instaleaza Docker pe serverul Rancher
  2. Instaleaza nfs-common pe nodurile K8s (client NFS)
  3. Porneste containerul Rancher
  4. Ruleaza init script-ul care: seteaza parola, creeaza API token, creeaza cluster-ul RKE2 via API, genereaza comanda de join
  5. Executa comanda de join pe fiecare nod K8s cu --etcd --controlplane --worker

3.1 Variabile Rancher

# roles/rancher/defaults/main.yml

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

3.2 Task-uri 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 Script-ul -- Automatizarea Completa a API-ului Rancher

Aceasta este inima automatizarii. Script-ul interactioneaza cu Rancher API v3 si v1 pentru a se autentifica, schimba parola, crea un API token, seta server-url, crea cluster-ul RKE2, si genera registration token.

Cluster-ul este creat cu aceasta cerere 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 Principal

# 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 Executie

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

Acest playbook dureaza ~10-15 minute. Rancher container-ul are nevoie de ~2-3 minute sa porneasca, apoi fiecare nod are nevoie de ~3-5 minute pentru join si bootstrap-ul RKE2.


Faza 4: Verificare Cluster

4.1 Acces Rancher UI

Deschide https://RANCHER_SERVER_IP in browser. La prima accesare vei vedea un warning de certificat self-signed.

4.2 kubectl

# Verificare noduri
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

# Verificare pod-uri de sistem
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

Faza 5: NFS Subdir External Provisioner

Acum avem un cluster functional, dar fara storage dinamic. NFS Subdir External Provisioner creeaza automat subdirectoare pe NFS server pentru fiecare PersistentVolumeClaim.

5.1 Instalare cu 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 Verificare

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 Test -- Creeaza un 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!

# Cleanup
kubectl delete -f test-pvc.yaml

Rezumat Complet

| Faza | Tool | Ce Face | Timp | |------|------|---------|------| | 1 | Terraform | Provisioneaza VPC, servere, LB, firewall, volume | ~3 min | | 2 | Ansible | Instaleaza si configureaza NFS server | ~2 min | | 3 | Ansible | Instaleaza Docker, Rancher, creeaza cluster, join noduri | ~15 min | | 4 | kubectl | Verificare cluster health | ~1 min | | 5 | Helm | Instaleaza NFS provisioner pentru PV dinamice | ~1 min | | Total | | De la zero la cluster functional | ~22 min |


Tips pentru Productie

  1. Backup etcd: Configureaza backup automat etcd -- pierderea etcd inseamna pierderea clusterului. RKE2 suporta snapshots automate la /var/lib/rancher/rke2/server/db/snapshots.

  2. Rancher HA: Pentru productie, ruleaza Rancher pe un cluster RKE2 dedicat cu Helm, nu in Docker single-container.

  3. cert-manager: Instaleaza cert-manager + Let's Encrypt pentru certificate TLS automate pe ingress-ul RKE2 NGINX.

  4. Monitoring: RKE2 vine cu metrics-server preinstalat. Adauga Prometheus + Grafana din Rancher Marketplace.

  5. Network Policies: Calico suporta NetworkPolicies native. Defineste politici de izolare intre namespace-uri din start.

  6. Hetzner Volume backup: Hetzner nu face backup automat la Volumes. Configureaza un cron job cu rsync sau snapshot-uri periodice.

  7. Scaling: Pentru a adauga noduri, schimba k8s_count in Terraform, ruleaza apply, actualizeaza inventory-ul Ansible, si ruleaza playbook-ul rancher. Noile noduri se vor joina automat.


Resurse


Acest articol este bazat pe o configuratie reala de productie. Codul Terraform si Ansible este testat si folosit activ pentru provisionarea clusterelor de development.