## 📋 1. PREREQUISITES - Ubuntu/Debian server (or your local Linux) - Hetzner Cloud account - SSH access --- ## 🔑 2. GETTING HETZNER API TOKEN 1. Go to https://console.hetzner.cloud/ 2. Select or create a project 3. Security → API Tokens → Generate API Token 4. Name: `terraform-homelab` 5. Permissions: **Read & Write** 6. **Copy the token** (shown only once!) --- ## ⚡ 3. AUTOMATED INSTALLATION (RECOMMENDED) ### 3.1 Download and Run Installer ```bash # Download the script curl -O https://raw.githubusercontent.com/yourusername/hetzner-k3s-lab/main/install.sh # OR create manually (see below) ``` ### 3.2 Create the Installer ````bash cat > ~/install-hetzner-k3s.sh << 'INSTALLER_EOF' #!/bin/bash # # Hetzner K3s Lab - Automated Installer # Description: Automatically deploys a 3-node K3s cluster on Hetzner Cloud # set -e # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # Configuration PROJECT_DIR="${HOME}/hetzner-lab" LOG_FILE="${PROJECT_DIR}/install.log" # Functions log_info() { echo -e "${GREEN}[INFO]${NC} $1" | tee -a "$LOG_FILE" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" | tee -a "$LOG_FILE" } log_error() { echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE" } log_step() { echo -e "\n${BLUE}===================================${NC}" echo -e "${BLUE}$1${NC}" echo -e "${BLUE}===================================${NC}\n" } check_requirements() { log_step "Checking Requirements" if [ "$EUID" -eq 0 ]; then log_error "Please do not run as root" exit 1 fi if ! ping -c 1 google.com &> /dev/null; then log_error "No internet connection" exit 1 fi log_info "Requirements check passed" } install_dependencies() { log_step "Installing Dependencies" log_info "Updating system packages..." sudo apt-get update -qq log_info "Installing basic tools..." sudo apt-get install -y -qq \ gnupg \ software-properties-common \ curl \ wget \ unzip \ git \ jq \ python3-pip \ > /dev/null 2>&1 if ! command -v terraform &> /dev/null; then log_info "Installing Terraform..." wget -O- https://apt.releases.hashicorp.com/gpg | \ sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \ sudo tee /etc/apt/sources.list.d/hashicorp.list sudo apt-get update -qq sudo apt-get install -y terraform > /dev/null 2>&1 else log_info "Terraform already installed: $(terraform version | head -n1)" fi if ! command -v ansible &> /dev/null; then log_info "Installing Ansible..." sudo apt-get install -y ansible > /dev/null 2>&1 else log_info "Ansible already installed: $(ansible --version | head -n1)" fi if ! command -v kubectl &> /dev/null; then log_info "Installing kubectl..." curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" 2>/dev/null sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl rm kubectl else log_info "kubectl already installed" fi log_info "All dependencies installed successfully" } setup_ssh_key() { log_step "Setting up SSH Key" if [ ! -f ~/.ssh/id_rsa ]; then log_info "Generating SSH key..." ssh-keygen -t rsa -b 4096 -N "" -f ~/.ssh/id_rsa -q log_info "SSH key generated" else log_info "SSH key already exists" fi } create_project_structure() { log_step "Creating Project Structure" if [ -d "$PROJECT_DIR" ]; then log_warn "Project directory already exists. Backing up..." mv "$PROJECT_DIR" "${PROJECT_DIR}.backup.$(date +%s)" fi mkdir -p "$PROJECT_DIR"/{terraform/{templates,},ansible/{inventory,roles/{k3s-install/tasks,k3s-reset/tasks},playbooks},scripts} log_info "Project structure created at: $PROJECT_DIR" } create_terraform_files() { log_step "Creating Terraform Configuration" cd "$PROJECT_DIR/terraform" cat > variables.tf << 'EOF' variable "hcloud_token" { description = "Hetzner Cloud API Token" type = string sensitive = true } variable "cluster_name" { description = "Name of the cluster" type = string default = "k3s-homelab" } variable "location" { description = "Hetzner location" type = string default = "nbg1" } variable "server_type_master" { description = "Server type for master" type = string default = "cx23" } variable "server_type_worker" { description = "Server type for workers" type = string default = "cx23" } variable "worker_count" { description = "Number of worker nodes" type = number default = 2 } variable "ssh_public_key_path" { description = "Path to SSH public key" type = string default = "~/.ssh/id_rsa.pub" } EOF cat > main.tf << 'EOF' terraform { required_version = ">= 1.5.0" required_providers { hcloud = { source = "hetznercloud/hcloud" version = "~> 1.45" } local = { source = "hashicorp/local" version = "~> 2.4" } } } provider "hcloud" { token = var.hcloud_token } resource "hcloud_ssh_key" "default" { name = "${var.cluster_name}-key" public_key = file(pathexpand(var.ssh_public_key_path)) } resource "hcloud_network" "private" { name = "${var.cluster_name}-network" ip_range = "10.0.0.0/16" } resource "hcloud_network_subnet" "subnet" { network_id = hcloud_network.private.id type = "cloud" network_zone = "eu-central" ip_range = "10.0.1.0/24" } resource "hcloud_firewall" "k3s" { name = "${var.cluster_name}-firewall" rule { direction = "in" protocol = "tcp" port = "22" source_ips = ["0.0.0.0/0", "::/0"] } rule { direction = "in" protocol = "tcp" port = "6443" source_ips = ["0.0.0.0/0", "::/0"] } rule { direction = "in" protocol = "tcp" port = "80" source_ips = ["0.0.0.0/0", "::/0"] } rule { direction = "in" protocol = "tcp" port = "443" source_ips = ["0.0.0.0/0", "::/0"] } } resource "hcloud_server" "k3s_master" { name = "${var.cluster_name}-master" server_type = var.server_type_master image = "ubuntu-22.04" location = var.location ssh_keys = [hcloud_ssh_key.default.id] firewall_ids = [hcloud_firewall.k3s.id] network { network_id = hcloud_network.private.id ip = "10.0.1.10" } user_data = <<-EOT #cloud-config hostname: ${var.cluster_name}-master package_update: true package_upgrade: true packages: - curl - wget - git - htop - vim - jq timezone: UTC runcmd: - swapoff -a - sed -i '/ swap / s/^/#/' /etc/fstab EOT labels = { type = "k3s-master" cluster = var.cluster_name } depends_on = [hcloud_network_subnet.subnet] } resource "hcloud_server" "k3s_workers" { count = var.worker_count name = "${var.cluster_name}-worker-${count.index + 1}" server_type = var.server_type_worker image = "ubuntu-22.04" location = var.location ssh_keys = [hcloud_ssh_key.default.id] firewall_ids = [hcloud_firewall.k3s.id] network { network_id = hcloud_network.private.id ip = "10.0.1.${20 + count.index}" } user_data = <<-EOT #cloud-config hostname: ${var.cluster_name}-worker-${count.index + 1} package_update: true package_upgrade: true packages: - curl - wget - git - htop - vim timezone: UTC runcmd: - swapoff -a - sed -i '/ swap / s/^/#/' /etc/fstab EOT labels = { type = "k3s-worker" cluster = var.cluster_name } depends_on = [hcloud_network_subnet.subnet] } resource "local_file" "ansible_inventory" { content = templatefile("${path.module}/templates/inventory.tpl", { master_ip = hcloud_server.k3s_master.ipv4_address worker_ips = [for w in hcloud_server.k3s_workers : w.ipv4_address] }) filename = "${path.root}/../ansible/inventory/hosts.yml" } resource "local_file" "server_ips" { content = jsonencode({ master_ip = hcloud_server.k3s_master.ipv4_address worker_ips = [for w in hcloud_server.k3s_workers : w.ipv4_address] }) filename = "${path.root}/../server_ips.json" } EOF cat > outputs.tf << 'EOF' output "master_ip" { description = "Master node public IP" value = hcloud_server.k3s_master.ipv4_address } output "worker_ips" { description = "Worker nodes public IPs" value = [for w in hcloud_server.k3s_workers : w.ipv4_address] } output "ssh_master" { description = "SSH to master" value = "ssh root@${hcloud_server.k3s_master.ipv4_address}" } output "cost_estimate" { description = "Cost estimates" value = { hourly = format("EUR %.4f", (var.worker_count + 1) * 0.005) two_hours = format("EUR %.4f", (var.worker_count + 1) * 0.005 * 2) daily = format("EUR %.2f", (var.worker_count + 1) * 0.005 * 24) monthly = format("EUR %.2f", (var.worker_count + 1) * 2.99) } } EOF mkdir -p templates cat > templates/inventory.tpl << 'EOF' all: vars: ansible_user: root ansible_ssh_common_args: '-o StrictHostKeyChecking=no' k3s_version: v1.28.5+k3s1 k3s_token: "homelab-secret-token-12345" children: k3s_cluster: children: k3s_master: hosts: master: ansible_host: ${master_ip} k3s_workers: hosts: %{ for idx, ip in worker_ips ~} worker-${idx + 1}: ansible_host: ${ip} %{ endfor ~} EOF cat > terraform.tfvars.example << 'EOF' hcloud_token = "YOUR_HETZNER_API_TOKEN_HERE" cluster_name = "k3s-homelab" location = "nbg1" server_type_master = "cx23" server_type_worker = "cx23" worker_count = 2 EOF log_info "Terraform configuration created" } create_ansible_files() { log_step "Creating Ansible Configuration" cd "$PROJECT_DIR/ansible" cat > ansible.cfg << 'EOF' [defaults] inventory = ./inventory/hosts.yml roles_path = ./roles remote_user = root host_key_checking = False retry_files_enabled = False gathering = smart timeout = 30 forks = 10 pipelining = True log_path = ./ansible.log display_skipped_hosts = False display_ok_hosts = True [ssh_connection] ssh_args = -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ControlMaster=auto -o ControlPersist=60s pipelining = True control_path = /tmp/ansible-ssh-%%h-%%p-%%r EOF cat > roles/k3s-install/tasks/main.yml << 'EOF' --- - name: Install K3s on master when: "'k3s_master' in group_names" block: - name: Download K3s install script get_url: url: https://get.k3s.io dest: /tmp/k3s-install.sh mode: '0700' - name: Get master public IP set_fact: master_public_ip: "{{ ansible_host }}" - name: Install K3s server shell: | INSTALL_K3S_VERSION={{ k3s_version }} \ K3S_TOKEN={{ k3s_token }} \ sh /tmp/k3s-install.sh server \ --disable traefik \ --tls-san {{ master_public_ip }} \ --write-kubeconfig-mode 644 args: creates: /usr/local/bin/k3s register: k3s_install - name: Wait for K3s to be ready wait_for: port: 6443 host: "{{ master_public_ip }}" delay: 10 timeout: 300 - name: Wait additional time for API server stability pause: seconds: 15 - name: Verify K3s is running systemd: name: k3s state: started enabled: yes - name: Test K3s API is responding command: kubectl get nodes register: k3s_nodes retries: 5 delay: 5 until: k3s_nodes.rc == 0 changed_when: false - name: Fetch kubeconfig fetch: src: /etc/rancher/k3s/k3s.yaml dest: "{{ playbook_dir }}/../kubeconfig" flat: yes - name: Install K3s on workers when: "'k3s_workers' in group_names" block: - name: Download K3s install script get_url: url: https://get.k3s.io dest: /tmp/k3s-install.sh mode: '0700' - name: Get master public IP from inventory set_fact: master_public_ip: "{{ hostvars['master']['ansible_host'] }}" - name: Display connection info debug: msg: "Connecting worker {{ inventory_hostname }} to K3s master at {{ master_public_ip }}:6443" - name: Install K3s agent shell: | K3S_URL=https://{{ master_public_ip }}:6443 \ K3S_TOKEN={{ k3s_token }} \ INSTALL_K3S_VERSION={{ k3s_version }} \ sh /tmp/k3s-install.sh agent args: creates: /usr/local/bin/k3s-agent register: k3s_agent_install retries: 3 delay: 10 until: k3s_agent_install.rc == 0 - name: Verify K3s agent is running systemd: name: k3s-agent state: started enabled: yes - name: Wait for agent to join cluster pause: seconds: 10 EOF cat > roles/k3s-reset/tasks/main.yml << 'EOF' --- - name: Uninstall K3s from all nodes block: - name: Stop K3s services systemd: name: "{{ item }}" state: stopped loop: - k3s - k3s-agent ignore_errors: yes - name: Run K3s uninstall script (server) command: /usr/local/bin/k3s-uninstall.sh when: "'k3s_master' in group_names" ignore_errors: yes - name: Run K3s uninstall script (agent) command: /usr/local/bin/k3s-agent-uninstall.sh when: "'k3s_workers' in group_names" ignore_errors: yes - name: Clean up remaining files file: path: "{{ item }}" state: absent loop: - /etc/rancher - /var/lib/rancher - /var/lib/kubelet - /tmp/k3s-install.sh ignore_errors: yes - name: Remove K3s binaries file: path: "{{ item }}" state: absent loop: - /usr/local/bin/k3s - /usr/local/bin/k3s-agent - /usr/local/bin/kubectl - /usr/local/bin/crictl - /usr/local/bin/ctr ignore_errors: yes EOF cat > playbooks/install-k3s.yml << 'EOF' --- - name: Install K3s cluster hosts: k3s_cluster become: yes roles: - k3s-install - name: Configure local kubectl access hosts: localhost connection: local gather_facts: no tasks: - name: Ensure .kube directory exists file: path: "{{ lookup('env', 'HOME') }}/.kube" state: directory mode: '0755' - name: Copy kubeconfig to local machine copy: src: "{{ playbook_dir }}/../kubeconfig" dest: "{{ lookup('env', 'HOME') }}/.kube/config-hetzner" mode: '0600' - name: Get master public IP from inventory set_fact: master_ip: "{{ hostvars['master']['ansible_host'] }}" - name: Update kubeconfig with correct master IP replace: path: "{{ lookup('env', 'HOME') }}/.kube/config-hetzner" regexp: 'https://[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:6443' replace: "https://{{ master_ip }}:6443" - name: Also replace localhost in kubeconfig replace: path: "{{ lookup('env', 'HOME') }}/.kube/config-hetzner" regexp: 'https://127\.0\.0\.1:6443' replace: "https://{{ master_ip }}:6443" - name: Display kubeconfig setup instructions debug: msg: - "======================================" - "K3s cluster deployed successfully!" - "======================================" - "" - "To use the cluster:" - " export KUBECONFIG={{ lookup('env', 'HOME') }}/.kube/config-hetzner" - " kubectl get nodes" - "" - "Or add to ~/.bashrc:" - " echo 'export KUBECONFIG={{ lookup('env', 'HOME') }}/.kube/config-hetzner' >> ~/.bashrc" - "" - "Master IP: {{ master_ip }}" - "" - name: Test kubectl connectivity shell: | export KUBECONFIG={{ lookup('env', 'HOME') }}/.kube/config-hetzner kubectl get nodes register: kubectl_output changed_when: false ignore_errors: yes - name: Display cluster nodes debug: msg: "{{ kubectl_output.stdout_lines }}" when: kubectl_output.rc == 0 - name: Warn if kubectl failed debug: msg: "WARNING: kubectl test failed. Please check kubeconfig manually." when: kubectl_output.rc != 0 EOF cat > playbooks/reset-k3s.yml << 'EOF' --- - name: Reset K3s cluster hosts: k3s_cluster become: yes roles: - k3s-reset - name: Clean local kubeconfig hosts: localhost connection: local gather_facts: no tasks: - name: Remove local kubeconfig file: path: "{{ lookup('env', 'HOME') }}/.kube/config-hetzner" state: absent - name: Remove project kubeconfig file: path: "{{ playbook_dir }}/../kubeconfig" state: absent - name: Display reset complete message debug: msg: - "======================================" - "K3s cluster reset completed" - "======================================" EOF log_info "Ansible configuration created" } create_scripts() { log_step "Creating Management Scripts" cd "$PROJECT_DIR/scripts" cat > lab.sh << 'LAB_EOF' #!/bin/bash set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" GREEN='\033[0;32m' YELLOW='\033[1;33m' RED='\033[0;31m' BLUE='\033[0;34m' NC='\033[0m' log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_step() { echo -e "\n${BLUE}===================================${NC}" echo -e "${BLUE}$1${NC}" echo -e "${BLUE}===================================${NC}\n" } check_token() { if [ ! -f "$PROJECT_ROOT/terraform/terraform.tfvars" ]; then log_error "terraform.tfvars not found!" log_info "Run: cd $PROJECT_ROOT/terraform && cp terraform.tfvars.example terraform.tfvars" log_info "Then edit terraform.tfvars and add your Hetzner API token" exit 1 fi } start_lab() { log_step "Starting Hetzner K3s Lab" check_token cd "$PROJECT_ROOT/terraform" log_info "Initializing Terraform..." terraform init -upgrade log_info "Planning infrastructure..." terraform plan -out=tfplan log_warn "Review the plan above. Continue? (yes/no)" read -r confirm if [ "$confirm" != "yes" ]; then log_error "Cancelled" exit 0 fi log_info "Creating infrastructure..." terraform apply tfplan log_info "Infrastructure created!" terraform output log_info "Waiting 60 seconds for servers to boot..." sleep 60 log_info "Deploying K3s with Ansible..." cd "$PROJECT_ROOT/ansible" log_info "Testing SSH connectivity..." if ansible all -m ping; then log_info "SSH connectivity OK" else log_error "SSH connectivity failed. Waiting 30 more seconds..." sleep 30 if ! ansible all -m ping; then log_error "SSH still failing. Please check manually." exit 1 fi fi log_info "Installing K3s cluster..." ansible-playbook playbooks/install-k3s.yml log_step "Lab Ready!" log_info "" log_info "To use the cluster:" log_info " export KUBECONFIG=~/.kube/config-hetzner" log_info " kubectl get nodes" log_info "" log_info "Or add to ~/.bashrc:" log_info " echo 'export KUBECONFIG=~/.kube/config-hetzner' >> ~/.bashrc" log_info "" } stop_lab() { log_step "Destroying Hetzner K3s Lab" log_warn "This will DELETE all servers. Continue? (type 'destroy')" read -r confirm if [ "$confirm" != "destroy" ]; then log_error "Cancelled" exit 0 fi if [ -f "$PROJECT_ROOT/ansible/inventory/hosts.yml" ]; then log_info "Resetting K3s cluster..." cd "$PROJECT_ROOT/ansible" ansible-playbook playbooks/reset-k3s.yml || log_warn "K3s reset had some errors (this is OK)" fi cd "$PROJECT_ROOT/terraform" terraform destroy -auto-approve log_info "Lab destroyed" } status_lab() { cd "$PROJECT_ROOT/terraform" if [ ! -f "terraform.tfstate" ]; then log_info "No active lab" exit 0 fi log_step "Lab Status" terraform output if [ -f "server_ips.json" ]; then MASTER_IP=$(jq -r '.master_ip' server_ips.json) log_info "Checking cluster health..." export KUBECONFIG=~/.kube/config-hetzner if kubectl get nodes 2>/dev/null; then log_info "Cluster is healthy" echo "" kubectl get nodes echo "" kubectl get pods -A else log_warn "Cannot connect to cluster via kubectl" log_info "Checking directly on master node..." if ssh -o ConnectTimeout=5 root@$MASTER_IP "kubectl get nodes" 2>/dev/null; then log_info "Cluster is running on master" else log_warn "Cannot connect to master" fi fi fi } ssh_master() { cd "$PROJECT_ROOT/terraform" if [ ! -f "server_ips.json" ]; then log_error "No active lab" exit 1 fi MASTER_IP=$(jq -r '.master_ip' server_ips.json) ssh root@$MASTER_IP } reinstall_k3s() { log_step "Reinstalling K3s" cd "$PROJECT_ROOT/ansible" log_info "Resetting K3s..." ansible-playbook playbooks/reset-k3s.yml log_info "Waiting 10 seconds..." sleep 10 log_info "Reinstalling K3s..." ansible-playbook playbooks/install-k3s.yml log_info "K3s reinstalled successfully" } show_help() { echo "Hetzner K3s Lab - Management Script" echo "" echo "Usage: $0 {start|stop|status|ssh|reinstall|help}" echo "" echo "Commands:" echo " start - Create infrastructure and deploy K3s cluster" echo " stop - Destroy cluster and all resources" echo " status - Show cluster status and information" echo " ssh - SSH to master node" echo " reinstall - Reset and reinstall K3s (keep infrastructure)" echo " help - Show this help message" echo "" } case "${1:-}" in start) start_lab ;; stop) stop_lab ;; status) status_lab ;; ssh) ssh_master ;; reinstall) reinstall_k3s ;; help) show_help ;; *) show_help exit 1 ;; esac LAB_EOF chmod +x lab.sh log_info "Management scripts created" } create_readme() { log_step "Creating Documentation" cat > "$PROJECT_DIR/README.md" << 'README_EOF' # Hetzner K3s Lab Automated 3-node Kubernetes cluster on Hetzner Cloud with Ansible. ## Quick Start 1. **Configure Hetzner API Token**: ```bash cd ~/hetzner-lab/terraform cp terraform.tfvars.example terraform.tfvars nano terraform.tfvars # Add your token ```` 2. **Deploy Cluster**: ```bash cd ~/hetzner-lab/scripts ./lab.sh start ``` 3. **Use Cluster**: ```bash export KUBECONFIG=~/.kube/config-hetzner kubectl get nodes ``` 4. **Destroy Cluster**: ```bash ./lab.sh stop ``` ## Commands ```bash ./lab.sh start # Deploy cluster (Terraform + Ansible) ./lab.sh stop # Destroy cluster and all resources ./lab.sh status # Show cluster status ./lab.sh ssh # SSH to master node ./lab.sh reinstall # Reinstall K3s without recreating servers ./lab.sh help # Show help ``` ## Features ✅ Fully automated deployment with Terraform + Ansible ✅ Universal kubeconfig management (no manual IP updates) ✅ Automatic retry logic for worker nodes ✅ Clean cluster reset and reinstallation ✅ SSH connectivity testing before deployment ✅ Comprehensive error handling ## Cost - **Hourly**: EUR 0.015 - **2 hours**: EUR 0.03 - **Daily**: EUR 0.36 - **Monthly**: EUR 8.97 ## Architecture ``` Master (CX23) + 2x Workers (CX23) ├── Terraform: Infrastructure provisioning ├── Ansible: K3s installation and configuration └── Private Network (10.0.0.0/16) ``` ## Troubleshooting If kubectl cannot connect after deployment: ```bash cd ~/hetzner-lab/scripts ./lab.sh reinstall ``` README_EOF ``` log_info "Documentation created" ``` } setup_hetzner_token() { log_step "Configuring Hetzner API Token" ``` cd "$PROJECT_DIR/terraform" cp terraform.tfvars.example terraform.tfvars log_warn "Please enter your Hetzner API Token:" read -r HCLOUD_TOKEN cat > terraform.tfvars << EOF ``` hcloud_token = "$HCLOUD_TOKEN" cluster_name = "k3s-homelab" location = "nbg1" server_type_master = "cx23" server_type_worker = "cx23" worker_count = 2 EOF ``` log_info "Token configured" ``` } main() { clear echo -e "${BLUE}" cat << 'BANNER' ╔═══════════════════════════════════════════════════╗ ║ ║ ║ Hetzner K3s Lab - Auto Installer ║ ║ ║ ║ Automated 3-node Kubernetes cluster with ║ ║ Terraform + Ansible (EUR 0.015/hour) ║ ║ ║ ╚═══════════════════════════════════════════════════╝ BANNER echo -e "${NC}\n" ``` mkdir -p "$(dirname "$LOG_FILE")" check_requirements install_dependencies setup_ssh_key create_project_structure create_terraform_files create_ansible_files create_scripts create_readme log_info "" log_warn "Do you want to configure your Hetzner API token now? (yes/no)" read -r configure_now if [ "$configure_now" == "yes" ]; then setup_hetzner_token log_info "" log_warn "Do you want to deploy the cluster now? (yes/no)" read -r deploy_now if [ "$deploy_now" == "yes" ]; then cd "$PROJECT_DIR/scripts" ./lab.sh start fi fi log_step "Installation Complete!" log_info "" log_info "Project created at: $PROJECT_DIR" log_info "" log_info "Next steps:" log_info "1. cd $PROJECT_DIR/terraform" log_info "2. cp terraform.tfvars.example terraform.tfvars" log_info "3. nano terraform.tfvars # Add your Hetzner API token" log_info "4. cd ../scripts && ./lab.sh start" log_info "" log_info "For help: ./lab.sh help" log_info "" ``` } main INSTALLER_EOF chmod +x ~/install-hetzner-k3s.sh # Run the installer ~/install-hetzner-k3s.sh ```` --- ## 💻 4. USING THE CLUSTER ### Quick Start ```bash cd ~/hetzner-lab/scripts # Create cluster (Terraform + Ansible) ./lab.sh start # Check status ./lab.sh status # SSH to master ./lab.sh ssh # Reinstall K3s only (keep servers) ./lab.sh reinstall # Destroy cluster ./lab.sh stop ```` ### Working with kubectl ```bash # Export kubeconfig (automatically configured by Ansible) export KUBECONFIG=~/.kube/config-hetzner # Check nodes kubectl get nodes # Check pods kubectl get pods -A # Deploy test application kubectl create deployment nginx --image=nginx --replicas=3 kubectl expose deployment nginx --port=80 --type=NodePort # Verify kubectl get pods kubectl get svc nginx ``` ### Monitoring the Cluster ```bash # Logs kubectl logs -f <pod-name> # Events kubectl get events --sort-by='.lastTimestamp' # Resource usage (if metrics-server is installed) kubectl top nodes kubectl top pods ``` --- ## 🗑️ 5. DESTROYING THE CLUSTER ### Via Script (Recommended) ```bash cd ~/hetzner-lab/scripts ./lab.sh stop ``` This will: 1. Reset K3s cluster with Ansible 2. Destroy all infrastructure with Terraform 3. Clean up local kubeconfig ### Via Terraform Only ```bash cd ~/hetzner-lab/terraform terraform destroy -auto-approve ``` ### Complete Cleanup ```bash # Remove entire project rm -rf ~/hetzner-lab # Remove kubeconfig rm ~/.kube/config-hetzner # Remove from .bashrc sed -i '/KUBECONFIG.*hetzner/d' ~/.bashrc ``` --- ## 🔧 6. MANUAL INSTALLATION (If You Need Control) ### 6.1 Install Dependencies ```bash # Update system sudo apt-get update && sudo apt-get upgrade -y # Install basic packages sudo apt-get install -y gnupg software-properties-common curl wget unzip git jq python3-pip # Install Terraform wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list sudo apt-get update sudo apt-get install -y terraform # Install Ansible sudo apt-get install -y ansible # Install kubectl curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl rm kubectl # Verify terraform version ansible --version kubectl version --client ``` ### 6.2 Create SSH Key ```bash ssh-keygen -t rsa -b 4096 -N "" -f ~/.ssh/id_rsa ``` ### 6.3 Deploy with Ansible ```bash cd ~/hetzner-lab # Configure Terraform cd terraform cp terraform.tfvars.example terraform.tfvars nano terraform.tfvars # Add your token # Deploy infrastructure terraform init terraform plan -out=tfplan terraform apply tfplan # Wait for servers to boot sleep 60 # Test Ansible connectivity cd ../ansible ansible all -m ping # Install K3s with Ansible ansible-playbook playbooks/install-k3s.yml # Use cluster export KUBECONFIG=~/.kube/config-hetzner kubectl get nodes ``` --- ## 🔍 7. TROUBLESHOOTING ### Issue: Ansible Cannot Connect ```bash # Check SSH keys are removed cd ~/hetzner-lab/terraform MASTER_IP=$(terraform output -raw master_ip) ssh-keygen -R "$MASTER_IP" # Test connection ssh -o StrictHostKeyChecking=no root@$MASTER_IP "hostname" # Retry Ansible cd ../ansible ansible all -m ping ``` ### Issue: Workers Not Joining ```bash # Check worker logs cd ~/hetzner-lab/terraform WORKER1_IP=$(terraform output -json worker_ips | jq -r '.[0]') ssh root@$WORKER1_IP "journalctl -u k3s-agent -n 50" # Reinstall K3s cd ../scripts ./lab.sh reinstall ``` ### Issue: kubectl Cannot Connect ```bash # The kubeconfig is automatically updated by Ansible # If still having issues, reinstall: cd ~/hetzner-lab/scripts ./lab.sh reinstall # Or manually fix kubeconfig: cd ~/hetzner-lab/terraform MASTER_IP=$(terraform output -raw master_ip) sed -i "s/https:\/\/[0-9.]*:6443/https:\/\/$MASTER_IP:6443/g" ~/.kube/config-hetzner ``` ### Issue: Terraform State Locked ```bash cd ~/hetzner-lab/terraform terraform force-unlock <LOCK_ID> ``` ### Issue: K3s Installation Hangs ```bash # Cancel the playbook (Ctrl+C) # Check Ansible logs cd ~/hetzner-lab/ansible tail -f ansible.log # Try with verbose mode ansible-playbook playbooks/install-k3s.yml -vvv ``` --- ## 📊 COST |Configuration|Hourly|2 Hours|Day|Month| |---|---|---|---|---| |3x CX23|EUR 0.015|EUR 0.03|EUR 0.36|EUR 8.97| --- ## ✅ CHECKLIST - [ ] Terraform installed - [ ] Ansible installed - [ ] kubectl installed - [ ] SSH key created - [ ] Hetzner API token obtained - [ ] terraform.tfvars configured - [ ] Infrastructure created - [ ] Ansible connectivity tested - [ ] K3s installed via Ansible - [ ] kubectl configured automatically - [ ] Cluster working - [ ] **terraform destroy executed (don't forget!)** --- ## 🎯 QUICK REFERENCE ```bash # Installation (one-time) bash ~/install-hetzner-k3s.sh # Create cluster (Terraform + Ansible) cd ~/hetzner-lab/scripts && ./lab.sh start # Use cluster export KUBECONFIG=~/.kube/config-hetzner kubectl get nodes # Reinstall K3s only ./lab.sh reinstall # Destroy cluster ./lab.sh stop ``` --- ## 🌟 KEY IMPROVEMENTS **Ansible Integration:** - ✅ Automated K3s installation with retry logic - ✅ Universal kubeconfig management - ✅ Automatic IP updates in kubeconfig - ✅ Clean cluster reset functionality - ✅ SSH connectivity testing before deployment **Operational Benefits:** - ✅ No manual IP management required - ✅ Repeatable deployments - ✅ Easy troubleshooting with `reinstall` command - ✅ Comprehensive error handling - ✅ Ansible logs for debugging --- **Done! You now have a fully automated K3s lab with Terraform + Ansible!** 🎉