Create low price kubernetes for home pets


Using hetzner.com, server type cx32 4 vcpu, 8 Gb memory, 6.30/month euro.

Prepare server

Terraform configs

Main file

File main.tf

terraform {
  required_providers {
    hcloud = {
      source  = "hetznercloud/hcloud"
      version = "~> 1.45.0"
    }
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.23.0"
    }
  }
}

# Configure the Hetzner Cloud Provider
provider "hcloud" {
  token = var.hcloud_token
}

resource "hcloud_ssh_key" "ssh" {
  name       = "My ssh key"
  public_key = file("~/.ssh/id_rsa.pub")
}

Locals

File locals.tf

# this file contains immutable constants
locals {
  http_port     = 80
  https_port    = 443
  all_ips       = ["0.0.0.0/0", "::/0"]
  ssh_port      = 3300
  tcp_protocol  = "tcp"
  udp_protocol  = "udp"
  any_protocol  = "-1"
  icmp_protocol = "icmp"
  vpn_traffic   = "17100"
  mail_smtp     = 25
  mail_imaps    = 993
  mail_smtps    = 465
  mail_smtptls  = 587

  network_cidr  = "10.0.0.0/16"
  network_subnet= "10.0.0.0/24"
  network_zone = "eu-central"

  image         = "ubuntu-24.04"
  server_type   = "cx32"

  home_ips       = ["<home_ip>/32"]
  cluster_ips   = ["<cluster_ip>/32"]
  trusted_ips    = concat(local.home_ips, local.cluster_ips)

  location = "nbg1"
  hcloud_token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}

Firewall

File firewall.tf

# allow incoming icmp protocol from all
resource "hcloud_firewall" "allow_icmp_trusted" {
  name = "allow_icmp_trusted"
  rule {
    direction  = "in"
    protocol   = "icmp"
    source_ips = local.trusted_ips
  }
}

# allow https connection from trusted ip, see locals.tf
resource "hcloud_firewall" "allow_http_https" {
  name = "allow_http_https"
  rule {
    direction  = "in"
    protocol   = local.tcp_protocol
    port       = local.https_port
    source_ips = local.all_ips
  }
  rule {
    direction  = "in"
    protocol   = local.tcp_protocol
    port       = local.http_port
    source_ips = local.all_ips
  }
}

# allow ssh connection from trusted ip, see locals.tf
resource "hcloud_firewall" "allow_ssh_trusted" {
  name = "allow_ssh_trusted"
  rule {
    direction  = "in"
    protocol   = local.tcp_protocol
    port       = local.ssh_port
    source_ips = local.trusted_ips
  }
}

User init

File userdata/k8s.sh

#!/bin/bash

# Add user
useradd -m devops -s /bin/bash
cp -Rp /root/.ssh /home/devops/ && chown -R devops:devops /home/devops/
echo "devops ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/devops

# Configure ssh to non standart post
cat > /etc/ssh/sshd_config.d/sshd.conf << EOF
Port 3300
PermitRootLogin no
PasswordAuthentication no
EOF

systemctl restart ssh.service

# Ugrade
apt-get update && apt-get upgrade -y

# Install additions sofrware 
apt-get install ca-certificates curl apt-transport-https htop gnupg software-properties-common -y

# https://kubernetes.io/docs/setup/production-environment/container-runtimes/#install-and-configure-prerequisites
# sysctl params required by setup, params persist across reboots
cat > /etc/sysctl.d/k8s.conf <<EOF
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

# Apply sysctl params without reboot
sudo sysctl --system

cat > /etc/modules-load.d/k8s.conf <<EOF
overlay
br_netfilter
EOF

modprobe overlay
modprobe br_netfilter

# Added kubernetes repository
mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
chmod 644 /etc/apt/keyrings/kubernetes-apt-keyring.gpg # allow unprivileged APT programs to read this keyring

echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' > /etc/apt/sources.list.d/kubernetes.list
chmod 644 /etc/apt/sources.list.d/kubernetes.list   # helps tools such as command-not-found to work correctly

# Add the CRI-O repository
PROJECT_PATH=prerelease:/main
curl -fsSL https://pkgs.k8s.io/addons:/cri-o:/$PROJECT_PATH/deb/Release.key |
    gpg --dearmor -o /etc/apt/keyrings/cri-o-apt-keyring.gpg

echo "deb [signed-by=/etc/apt/keyrings/cri-o-apt-keyring.gpg] https://pkgs.k8s.io/addons:/cri-o:/$PROJECT_PATH/deb/ /" |
    tee /etc/apt/sources.list.d/cri-o.list

apt-get update

# Disable swap
sed -ri 's/^([^#].*?\sswap\s+sw\s+.*)$/# \1/g' /etc/fstab
swapoff -a

# Install kubernetes tools
apt-get install -y kubelet kubeadm kubectl cri-o
apt-mark hold kubelet kubeadm kubectl

systemctl daemon-reload
systemctl enable --now kubelet 
systemctl enable --now crio.service
systemctl start crio.service
systemctl start kubelet

shutdown -r now

Server

File nodes.tf

resource "hcloud_server" "this" {
  name        = "main"
  server_type = local.server_type
  image       = local.image
  location    = local.location
  user_data   = file("${path.module}/userdata/k8s.sh")

  public_net {
    ipv4_enabled = true
    ipv6_enabled = false
  }

  lifecycle {
    ignore_changes = [
      user_data
    ]
  }

  firewall_ids = [
    hcloud_firewall.allow_ssh_trusted.id,
    hcloud_firewall.allow_http_https.id,
    hcloud_firewall.allow_icmp_trusted.id
  ]

  ssh_keys = [
    hcloud_ssh_key.ssh.id
  ]
}

output "host_ip" {
  description = "Host ip"
  value = {
    endpoint = "ssh devopsx@${hcloud_server.this.ipv4_address} -p 3300"
  }
}

Apply terraform

terraform init
terraform apply

Setup kubernetes

Login to server

ssh devopsx@server_ip -p 3300

Init kubernetes cluster

kubeadm init --kubernetes-version=v1.30.2

export KUBECONFIG=/etc/kubernetes/admin.conf

kubectl taint nodes --all node-role.kubernetes.io/control-plane-

Setup network calico

kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/canal.yaml

Install MetalLB

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.5/config/manifests/metallb-native.yaml
IP=$(ip addr show $(netstat -rn | grep 'UG '| grep ' 0.0.0.0 '| head -1| awk '{print $8}') | grep 'inet ' | awk '{print $2}' | cut -f1 -d'/')

cat > /root/metalbl.yaml << EOF
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: production
  namespace: metallb-system
spec:
  addresses:
  - ${IP}/32
EOF

kubectl -n metallb-system apply -f /root/metalbl.yaml