Stop Paying $150/Month for Managed Kubernetes - Run Your Own for $10

Stop Paying $150/Month for Managed Kubernetes - Run Your Own for $10

Table of Contents Task Assumptions Flow Init or Restore Init Delete Restore Price Info Install hcloud Install kubectl Install HELM Install AZ (azure CLI) for Linux Mint/DEB AZURE Set AZURE defaults Create the storage Create the blob container Velero credentials file Velero CLI Install CLI Install Velero on k3s List Backup File structure 1. init_cluster.sh 2. .env 3. cluster.yml 4. values_traefik-default.yml 5. values_kube-prometheus-stack.yml 6. gateway-post.yml How to Run 1st Run - init Delete (Power Off) Restore (Power On) Costs 🔹Task ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Create the cheapest Kubernetes solution with the possibility to delete the Kubernetes cluster/instance and recreate it from scratch (as a DR solution).- Implement Power On/Off for the environment.- $0 cost for offline mode, except for storage space for S3 and volumes. 🔹Assumptions ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Use a Hetzner cloud instance as single Master/Worker Use the integrated Load Balancer in Traefik Gateway Use Kubernetes Gateway instead of Ingress Recreate the Kubernetes environment from a Velero backup Use your host file (/etc/hosts) as a DNS provider 🔹Flow ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Init or Restore ──────────────────────────────────────────── (1) Import Configuration (2) Activate empty Hetzner Project (3) Install Kubernetes k3scluster on single node as Master/Worker with hetzner-k3s (4) Install Velero backup by HELM with AZURE plugin and AZURE credentials Init ──────────────────────────────────────────── (5) Install Kubernetes CRD Gateway API (6) Install Traefik (7) Install kube-prometheus-stack with Helm (8) Deploy nginx reverse proxy (/prometheus, /grafana) (9a) Post steps: generate self-signed TLS cert → secret tls-traefik (9b) Apply gateway-post.yml (10) Print costs report (Hetzner servers + volumes) Update your /etc/hosts Delete ──────────────────────────────────────────── (1) Set all PVs to Retain (2) Make Velero Backup (3) Delete Hetzner Cloud instance Restore ──────────────────────────────────────────── (5) Wait for Velero to sync backups from Azure storage (6) Find latest completed backup (7) Create Velero restore (excludes: kube-system, kube-public, kube-node-lease, velero (8) Print costs report (Hetzner servers + volumes) Update your /etc/hosts Price ──────────────────────────────────────────── Print costs report (Hetzner servers + volumes) 🔹Info ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ For the purposes of this project, we need to have: Active a Hetzner account Installed the hcloud CLI Installed the kubectl CLI Installed the HELM CLI Installed the Velero CLI SSH key for server access Active a Azure account for an S3 bucket Installed the Azure CLI - az ⚠️ For this occasion, we will be using an S3 bucket on the Azure platform, but keep in mind that you can use this setup for any S3-compatible storage. ⚠️ A list of supported providers for Velero can be found at https://velero.io/docs/main/supported-providers/ ⚠️ Depending on your provider, adapt this installation according to your requirements. 🔹Install hcloud ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ℹ️ GitHub releases: https://github.com/hetznercloud/cli/releases Check if the current version exists hcloud version Enter fullscreen mode Exit fullscreen mode Download to /tmp wget $(curl -s https://api.github.com/repos/hetznercloud/cli/releases/latest \ | jq -r '.assets[0].browser_download_url' \ | sed 's%checksums.txt%hcloud-linux-amd64.tar.gz%g') \ -P /tmp/ Enter fullscreen mode Exit fullscreen mode Check tar.gz tar -tvzf /tmp/hcloud-linux-amd64.tar.gz Enter fullscreen mode Exit fullscreen mode -rw-r--r-- runner/docker 1075 2025-01-16 18:15 LICENSE -rw-r--r-- runner/docker 6811 2025-01-16 18:15 README.md -rwxr-xr-x runner/docker 15311000 2025-01-16 18:19 hcloud Enter fullscreen mode Exit fullscreen mode Extract to /usr/local/bin/ sudo tar -xvzf /tmp/hcloud-linux-amd64.tar.gz \ -C /usr/local/bin/ hcloud sudo chmod +x /usr/local/bin/hcloud Enter fullscreen mode Exit fullscreen mode Check command whereis hcloud which hcloud Enter fullscreen mode Exit fullscreen mode hcloud version Enter fullscreen mode Exit fullscreen mode hcloud 1.63.0 Enter fullscreen mode Exit fullscreen mode Add to BASH autocomplete vi ~/.bashrc Enter fullscreen mode Exit fullscreen mode # -- hcloud ------------------------------------------------------------------- source Enter fullscreen mode Exit fullscreen mode all image server-type certificate iso ssh-key completion load-balancer storage-box config load-balancer-type storage-box-type context location version datacenter network volume firewall placement-group zone floating-ip primary-ip help server Enter fullscreen mode Exit fullscreen mode 🔹Install kubectl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ℹ️ See https://kubernetes.io/docs/tasks/tools/ 🔹Install HELM ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Url: ℹ️ Official Site: https://helm.sh/ ℹ️ How to Install: https://helm.sh/docs/intro/install/ Install cd /tmp curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 chmod +x get_helm.sh sudo ./get_helm.sh Enter fullscreen mode Exit fullscreen mode Helm v4.1.4 is available. Changing from version v3.19.4. Downloading https://get.helm.sh/helm-v4.1.4-linux-amd64.tar.gz Verifying checksum... Done. Preparing to install helm into /usr/local/bin helm installed into /usr/local/bin/helm Enter fullscreen mode Exit fullscreen mode Cleanup sudo rm -fv /tmp/get_helm.sh Enter fullscreen mode Exit fullscreen mode Check version /usr/local/bin/helm version --short Enter fullscreen mode Exit fullscreen mode v4.1.4+g05fa379 Enter fullscreen mode Exit fullscreen mode Add $PATH and autocomplete to ~/.bashrc vi ~/.bashrc Enter fullscreen mode Exit fullscreen mode # -- HELM --------------------------------------------------------------------- export PATH="$PATH:/usr/local/bin" source /dev/null sudo chmod go+r /etc/apt/keyrings/microsoft.gpg Enter fullscreen mode Exit fullscreen mode Create repository AZ_DIST=$(lsb_release -cs) echo "Types: deb URIs: https://packages.microsoft.com/repos/azure-cli/ Suites: ${AZ_DIST} Components: main Architectures: $(dpkg --print-architecture) Signed-by: /etc/apt/keyrings/microsoft.gpg" | sudo tee /etc/apt/sources.list.d/azure-cli.sources Enter fullscreen mode Exit fullscreen mode APT sudo apt-get update sudo apt-get install azure-cli Enter fullscreen mode Exit fullscreen mode Upgrade sudo apt-get update sudo apt-get install --only-upgrade azure-cli Enter fullscreen mode Exit fullscreen mode Check version az version Enter fullscreen mode Exit fullscreen mode { "azure-cli": "2.85.0", "azure-cli-core": "2.85.0", "azure-cli-telemetry": "1.1.0", "extensions": { "bastion": "1.4.3", "ssh": "2.0.6" } Enter fullscreen mode Exit fullscreen mode AZURE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🔹Set AZURE defaults ──────────────────────────────────────────── az login az login Enter fullscreen mode Exit fullscreen mode Set default subscription az account list --output table Enter fullscreen mode Exit fullscreen mode Name CloudName SubscriptionId TenantId State IsDefault ------- ----------- ------------------------------------ ------------------------------------ ------- ----------- default AzureCloud xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy Enabled True AzureCloud xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy Enabled True Enter fullscreen mode Exit fullscreen mode Set default subscription | by SubscriptionId az account set --subscription "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" Set default location | West Europe az configure --list-defaults az configure --defaults location=westeurope az configure --list-defaults Enter fullscreen mode Exit fullscreen mode Create resource group az group list -o table az group create -n velero --location westeurope az group list -o table az group show --name velero --output table Enter fullscreen mode Exit fullscreen mode Location Name ---------- ------ westeurope velero Enter fullscreen mode Exit fullscreen mode 🔹Create the storage ──────────────────────────────────────────── az login az login AZURE_SUBSCRIPTION_ID=$(az account list --all --query '[?isDefault].id' -o tsv) AZURE_TENANT_ID=$(az account list --all --query '[?isDefault].tenantId' -o tsv) AZURE_BACKUP_RESOURCE_GROUP=velero BLOB_CONTAINER=edok3s AZURE_STORAGE_ACCOUNT_ID="velero$(uuidgen | cut -d '-' -f5 | tr '[A-Z]' '[a-z]')" Enter fullscreen mode Exit fullscreen mode Create the storage account az storage account create \ --name $AZURE_STORAGE_ACCOUNT_ID \ --resource-group $AZURE_BACKUP_RESOURCE_GROUP \ --sku Standard_LRS \ --encryption-services blob \ --https-only true \ --min-tls-version TLS1_2 \ --kind BlobStorage \ --access-tier Hot Enter fullscreen mode Exit fullscreen mode { "accessTier": "Hot", "accountMigrationInProgress": null, "allowBlobPublicAccess": false, "allowCrossTenantReplication": false, "allowSharedKeyAccess": null, "allowedCopyScope": null, "azureFilesIdentityBasedAuthentication": null, "blobRestoreStatus": null, "creationTime": "2026-05-03T19:30:06.161353+00:00", "customDomain": null, "defaultToOAuthAuthentication": null, "dnsEndpointType": null, "dualStackEndpointPreference": null, "enableExtendedGroups": null, "enableHttpsTrafficOnly": true, "enableNfsV3": null, "encryption": { "encryptionIdentity": null, "keySource": "Microsoft.Storage", "keyVaultProperties": null, "requireInfrastructureEncryption": null, "services": { "blob": { "enabled": true, "keyType": "Account", "lastEnabledTime": "2026-05-03T19:30:06.574215+00:00" }, "file": { "enabled": true, "keyType": "Account", "lastEnabledTime": "2026-05-03T19:30:06.574215+00:00" }, "queue": null, "table": null } }, "extendedLocation": null, "failoverInProgress": null, "geoPriorityReplicationStatus": null, "geoReplicationStats": null, "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/velero/providers/Microsoft.Storage/storageAccounts/velero44dfh567h5gh", "identity": null, "immutableStorageWithVersioning": null, "isHnsEnabled": null, "isLocalUserEnabled": null, "isSftpEnabled": null, "isSkuConversionBlocked": null, "keyCreationTime": { "key1": "2026-05-03T19:30:06.564067+00:00", "key2": "2026-05-03T19:30:06.564067+00:00" }, "keyPolicy": null, "kind": "BlobStorage", "largeFileSharesState": null, "lastGeoFailoverTime": null, "location": "westeurope", "minimumTlsVersion": "TLS1_2", "name": "velero44dfh567h5gh", "networkRuleSet": { "bypass": "AzureServices", "defaultAction": "Allow", "ipRules": [], "ipv6Rules": [], "resourceAccessRules": null, "virtualNetworkRules": [] }, "placement": null, "primaryEndpoints": { "blob": "https://velero44dfh567h5gh.blob.core.windows.net/", "dfs": "https://velero44dfh567h5gh.dfs.core.windows.net/", "file": null, "internetEndpoints": null, "ipv6Endpoints": null, "microsoftEndpoints": null, "queue": null, "table": "https://velero44dfh567h5gh.table.core.windows.net/", "web": null }, "primaryLocation": "westeurope", "privateEndpointConnections": [], "provisioningState": "Succeeded", "publicNetworkAccess": null, "resourceGroup": "velero", "routingPreference": null, "sasPolicy": null, "secondaryEndpoints": null, "secondaryLocation": null, "sku": { "name": "Standard_LRS", "tier": "Standard" }, "statusOfPrimary": "available", "statusOfSecondary": null, "storageAccountSkuConversionStatus": null, "tags": {}, "type": "Microsoft.Storage/storageAccounts", "zones": null } Enter fullscreen mode Exit fullscreen mode List all storage az storage account list --output table Enter fullscreen mode Exit fullscreen mode AccessTier AllowBlobPublicAccess AllowCrossTenantReplication CreationTime EnableHttpsTrafficOnly Kind Location MinimumTlsVersion Name PrimaryLocation ProvisioningState ResourceGroup StatusOfPrimary ------------ ----------------------- ----------------------------- -------------------------------- ------------------------ ----------- ---------- ------------------- ------------------ ----------------- ------------------- --------------- ----------------- Hot False False 2026-05-03T19:30:06.161353+00:00 True BlobStorage westeurope TLS1_2 velero44dfh567h5gh westeurope Succeeded velero available Enter fullscreen mode Exit fullscreen mode List storage # --name "$AZURE_STORAGE_ACCOUNT_ID" \ az storage account show \ --name "velero44dfh567h5gh" \ --resource-group "$AZURE_BACKUP_RESOURCE_GROUP" \ --query "{name:name, location:location, sku:sku.name, kind:kind, accessTier:accessTier, httpsOnly:enableHttpsTrafficOnly, minTls:minimumTlsVersion}" \ --output table Enter fullscreen mode Exit fullscreen mode Name Location Sku Kind AccessTier HttpsOnly MinTls ------------------ ---------- ------------ ----------- ------------ ----------- -------- velero44dfh567h5gh westeurope Standard_LRS BlobStorage Hot True TLS1_2 Enter fullscreen mode Exit fullscreen mode 🔹Create the blob container ──────────────────────────────────────────── Pull the storage account key once ACCOUNT_KEY=$(az storage account keys list \ --account-name "$AZURE_STORAGE_ACCOUNT_ID" \ --resource-group "$AZURE_BACKUP_RESOURCE_GROUP" \ --query "[0].value" -o tsv) echo ${ACCOUNT_KEY} Enter fullscreen mode Exit fullscreen mode bhsfgZJd-gkjae79kdgmkasfjk+fgghghflks56GJKJDSSA67jdd+htghlllsjhre46hgfdd2klj3i45fo45QW== Enter fullscreen mode Exit fullscreen mode Create the storage account az storage container create \ --account-name "$AZURE_STORAGE_ACCOUNT_ID" \ --name "$BLOB_CONTAINER" \ --public-access off \ --account-key "$ACCOUNT_KEY" Enter fullscreen mode Exit fullscreen mode Verify az storage container list \ --account-name "$AZURE_STORAGE_ACCOUNT_ID" \ --account-key "$ACCOUNT_KEY" \ --output table Enter fullscreen mode Exit fullscreen mode Name Lease Status Last Modified ------ -------------- ------------------------- edok3s 2026-05-03T19:56:01+00:00 Enter fullscreen mode Exit fullscreen mode 🔹Velero credentials file ──────────────────────────────────────────── Save the same key for Velero's use AZURE_STORAGE_ACCOUNT_ACCESS_KEY="$ACCOUNT_KEY" echo ${AZURE_STORAGE_ACCOUNT_ACCESS_KEY} .credentials-velero cat ./.credentials-velero AZURE_STORAGE_ACCOUNT_ACCESS_KEY=${AZURE_STORAGE_ACCOUNT_ACCESS_KEY} AZURE_CLOUD_NAME=AzurePublicCloud EOF Enter fullscreen mode Exit fullscreen mode Protect and list chmod 600 -v ./.credentials-velero ls -l cat .credentials-velero Enter fullscreen mode Exit fullscreen mode 🔹Velero CLI Release page: https://github.com/velero-io/velero/releases 🔹Install CLI Install CLI VELERO_VERSION=v1.18.0 # -- Download cd /tmp wget https://github.com/vmware-tanzu/velero/releases/download/${VELERO_VERSION}/velero-${VELERO_VERSION}-linux-amd64.tar.gz # -- Install ls -alh velero-${VELERO_VERSION}-linux-amd64.tar.gz tar -tvzf velero-${VELERO_VERSION}-linux-amd64.tar.gz tar -xvzf velero-${VELERO_VERSION}-linux-amd64.tar.gz sudo mv -v velero-${VELERO_VERSION}-linux-amd64/velero /usr/local/bin/ # -- Clean Up rm -rf /tmp/velero-${VELERO_VERSION}-linux-amd64* Enter fullscreen mode Exit fullscreen mode Check version velero version --client-only Enter fullscreen mode Exit fullscreen mode Client: Version: v1.18.0 Git commit: 6adcf06b5b0e6fb93998d3e101e2cbdc134fa3c3 Enter fullscreen mode Exit fullscreen mode 🔹Install Velero on k3s Path cd ~/Documents/hetzner-k3s/edo Enter fullscreen mode Exit fullscreen mode Env. variables See below .env file Kubeconfig source .env export KUBECONFIG="${HOME}/.kube/config-${CLUSTER_NAME}.yml" Enter fullscreen mode Exit fullscreen mode Check dependencies echo "VER_PLUGIN_AZURE=$VER_PLUGIN_AZURE" echo "AZURE_SUBSCRIPTION_ID=$AZURE_SUBSCRIPTION_ID" echo "AZURE_TENANT_ID=$AZURE_TENANT_ID" echo "AZURE_BACKUP_RESOURCE_GROUP=$AZURE_BACKUP_RESOURCE_GROUP" echo "BLOB_CONTAINER=$BLOB_CONTAINER" echo "AZURE_STORAGE_ACCOUNT_ID=$AZURE_STORAGE_ACCOUNT_ID" test -f .credentials-velero && echo "File exists: .credentials-velero" || f_error "File .credentials-velero DOES NOT EXIST" Enter fullscreen mode Exit fullscreen mode Check installation kubectl -n velero get all Enter fullscreen mode Exit fullscreen mode Check backupstoragelocation Available kubectl -n velero get backupstoragelocation default -o jsonpath='{.status.phase}' kubectl -n velero get backupstoragelocation default -o jsonpath='{.status.phase}' | grep -qx 'Available' Enter fullscreen mode Exit fullscreen mode Check Logs kubectl -n velero logs deployment/velero Enter fullscreen mode Exit fullscreen mode 🔹List Backup echo "----- List Velero Backups and Describe Last Backup ----------------------------" prev_count=0 for i in {1..30}; do curr_count=$(velero backup get -o json 2>/dev/null | jq '[.items[] | select(.status.phase == "Completed")] | length') if [[ "$curr_count" -gt 0 && "$curr_count" -eq "$prev_count" ]]; then break fi prev_count="$curr_count" echo " Attempt $i: Found $curr_count backups, waiting for sync to settle..." sleep 5 done velero backup get VELERO_LAST_BACKUP_NAME=$(velero backup get -o json 2>/dev/null | \ jq -r '[.items[] | select(.status.phase == "Completed")] | sort_by(.status.startTimestamp) | last | .metadata.name // empty') echo "" velero backup describe "${VELERO_LAST_BACKUP_NAME}" # velero backup logs "${VELERO_LAST_BACKUP_NAME}" echo "" Enter fullscreen mode Exit fullscreen mode 🔹File structure ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ File tree ├── cluster.yml # hetzner-k3s configuration file ├── .credentials-velero # AZIRE credentials for S3 ├── .env # Your configuration for init_cluster.sh ├── gateway-post.yml # k3s Gateway configuration ├── init_cluster.sh ├── kubeconfig # File create by hetzner-k3s --config. Don't use it ├── nginx.yml # Setup nginx reverse proxy for /promethes and /grafana ├── values_kube-prometheus-stack.yml # Prometheus/Grafana configuration └── values_traefik-default.yml # Traefik configuration Enter fullscreen mode Exit fullscreen mode 🔹1. init_cluster.sh #!/bin/bash # ----------------------------------------------------------------------------- # FLOW: # - Check for required arguments # - Source .env config; define helper functions (f_error, f_az_login_check) # If OPTION is "init" or "restore": # - Create k3s cluster with hetzner-k3s # - Get kubeconfig from cluster master and update server address to use public IP # - Install Velero with Azure plugin # If OPTION is "init": # - Install Traefik (Gateway API mode) # - Install kube-prometheus-stack with Helm # - Deploy nginx reverse proxy (nginx.yml: Namespace, ConfigMap, Secret, Service, Deployment) # - Post steps: generate self-signed TLS cert → secret tls-traefik; apply gateway-post.yml # If OPTION is "delete": # - Set all PVs to Retain # - Create Velero backup (TTL 6 months) # - Delete all servers (with confirmation prompt) # If OPTION is "restore": # - Wait for Velero to sync backups from Azure storage # - Find latest completed backup # - Create Velero restore (excludes: kube-system, kube-public, kube-node-lease, velero) # If OPTION is "init" or"costs": # - Print costs report (Hetzner servers + load balancers + volumes) # Note: costs report also runs automatically at the end of "init" # ----------------------------------------------------------------------------- # -- Check for required arguments --------------------------------------------- # Cluster name if [[ -z "${1:-}" ]]; then echo "Error: OPTION is required" >&2 echo "Usage: $0 " >&2 exit 1 fi # Power state must be 'on' or 'off' if [[ "$1" != "init" && "$1" != "delete" && "$1" != "restore" && "$1" != "costs" ]]; then echo "Error: OPTION must be 'init', 'delete', 'restore' or 'costs', got '$1'" >&2 exit 1 fi # ----------------------------------------------------------------------------- # -- Include Configs ---------------------------------------------------------- CONFIG_FILE="$(dirname "$0")/.env" [[ -f "$CONFIG_FILE" ]] || { echo "Error: $CONFIG_FILE not found"; exit 1; } # shellcheck source=/dev/null source "$CONFIG_FILE" # ----------------------------------------------------------------------------- # ----- f_error msg ----------------------------------------------------------- f_error() { echo "*** Error: $1" exit 1 } # ----------------------------------------------------------------------------- # ----- f_az_login_check ------------------------------------------------------ f_az_login_check() { local current_subscription_id current_tenant_id current_subscription_id=$(az account show --query id -o tsv 2>/dev/null) current_tenant_id=$(az account show --query tenantId -o tsv 2>/dev/null) [[ -z "$current_subscription_id" ]] && f_error "Not logged into Azure. Run: az login" [[ "$current_subscription_id" != "$AZURE_SUBSCRIPTION_ID" ]] && \ f_error "Azure subscription mismatch: current=$current_subscription_id, expected=$AZURE_SUBSCRIPTION_ID" [[ "$current_tenant_id" != "$AZURE_TENANT_ID" ]] && \ f_error "Azure tenant mismatch: current=$current_tenant_id, expected=$AZURE_TENANT_ID" echo "Azure login verified: subscription=$current_subscription_id, tenant=$current_tenant_id" } # ----------------------------------------------------------------------------- if [[ "$1" == "init" || "$1" == "restore" ]]; then # -- Create k3s ----------------------------------------------------------- hcloud context use edok3s || f_error "Failed to switch context to edok3s" echo "----- Hetzner Context List ---------------------------------------------------" hcloud context list echo "" echo "----- Create Cluster ----------------------------------------------------------" hetzner-k3s create --config "${HOME}/.kube/config-${CLUSTER_NAME}".yml echo "----- Kubeconfig --------------------------------------------------------------" cat ${HOME}/.kube/config-${CLUSTER_NAME}.yml export KUBECONFIG="${HOME}/.kube/config-${CLUSTER_NAME}.yml" echo "" echo "----- Test kubectl ------------------------------------------------------------" for i in {1..5}; do kubectl get all -A && break || { echo "Attempt $i/5 failed, retrying in 3s..."; sleep 3; } done || f_error "Failed to get all resources in cluster with kubectl" kubectl version || f_error "Failed to connect to cluster with kubectl" # -- Set pool=master label for control-plane nodes kubectl get nodes -l node-role.kubernetes.io/control-plane \ -o name | xargs -I{} kubectl label {} pool=master --overwrite || f_error "Failed to label control-plane nodes with pool=master" echo "" # ------------------------------------------------------------------------- # -- VELERO Install ------------------------------------------------------- echo "VER_PLUGIN_AZURE=$VER_PLUGIN_AZURE" echo "AZURE_SUBSCRIPTION_ID=$AZURE_SUBSCRIPTION_ID" echo "AZURE_TENANT_ID=$AZURE_TENANT_ID" echo "AZURE_BACKUP_RESOURCE_GROUP=$AZURE_BACKUP_RESOURCE_GROUP" echo "BLOB_CONTAINER=$BLOB_CONTAINER" echo "AZURE_STORAGE_ACCOUNT_ID=$AZURE_STORAGE_ACCOUNT_ID" test -f .credentials-velero && echo "File exists: .credentials-velero" || f_error "File .credentials-velero DOES NOT EXIST" velero install \ --provider azure \ --plugins velero/velero-plugin-for-microsoft-azure:"$VER_PLUGIN_AZURE" \ --bucket "$BLOB_CONTAINER" \ --secret-file .credentials-velero \ --backup-location-config "storageAccount=$AZURE_STORAGE_ACCOUNT_ID,storageAccountKeyEnvVar=AZURE_STORAGE_ACCOUNT_ACCESS_KEY,subscriptionId=$AZURE_SUBSCRIPTION_ID" \ --use-volume-snapshots=false \ --wait || f_error "Failed to install Velero with Azure plugin" # -- Add nodeSelector for Velero kubectl patch deployment velero -n velero -p \ '{"spec": {"template": {"spec": {"nodeSelector": {"pool": "master"}}}}}' kubectl -n velero get all if kubectl -n velero get backupstoragelocation default -o jsonpath='{.status.phase}' | grep -qx 'Available'; then echo "Velero backupstoragelocation Available" else kubectl -n velero logs deployment/velero f_error "Velero backupstoragelocation Not Available" fi # ------------------------------------------------------------------------- fi ###### OPTION: init ################################################################################################## if [[ "$1" == "init" ]]; then # -- 01 FUNCTION: Traefik Install ----------------------------------------- f_Traefik() { helm repo add traefik https://traefik.github.io/charts helm repo update # -- Gateway API CRDs (required; chart no longer ships them in future versions) kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/latest/download/standard-install.yaml || f_error "Failed to install Gateway API CRDs" # -- Install Traefik with custom values # --skip-crds \ helm upgrade --install traefik traefik/traefik \ -n traefik \ --create-namespace \ --wait --timeout 5m \ -f /dev/null | jq '[.items[] | select(.status.phase == "Completed")] | length') if [[ "$curr_count" -gt 0 && "$curr_count" -eq "$prev_count" ]]; then break fi prev_count="$curr_count" echo " Attempt $i: Found $curr_count backups, waiting for sync to settle..." sleep 5 done velero backup get VELERO_LAST_BACKUP_NAME=$(velero backup get -o json 2>/dev/null | \ jq -r '[.items[] | select(.status.phase == "Completed")] | sort_by(.status.startTimestamp) | last | .metadata.name // empty') echo "" velero backup describe "${VELERO_LAST_BACKUP_NAME}" # velero backup logs "${VELERO_LAST_BACKUP_NAME}" echo "" } # ------------------------------------------------------------------------- # -- FUNCTION: Delete all servers in current project ---------------------- f_Delete_Server() { local servers servers=$(hcloud server list -o noheader -o columns=name) if [[ -z "$servers" ]]; then echo "*** No servers found in current project" return fi echo "*** Servers to delete:" echo "$servers" echo "" read -r -p "Delete all servers listed above? [y/N] " reply [[ "${reply,,}" == "y" ]] || { echo "Aborted."; return; } while IFS= read -r server; do echo "*** Deleting server: $server ..." hcloud server delete "$server" || echo "Warning: failed to delete $server" & done /dev/null | jq '[.items[] | select(.status.phase == "Completed")] | length') if [[ "$curr_count" -gt 0 && "$curr_count" -eq "$prev_count" ]]; then break fi prev_count="$curr_count" echo " Attempt $i: Found $curr_count backups, waiting for sync to settle..." sleep 5 done VELERO_LAST_BACKUP_NAME=$(velero backup get -o json 2>/dev/null | \ jq -r '[.items[] | select(.status.phase == "Completed")] | sort_by(.status.startTimestamp) | last | .metadata.name // empty') [[ -z "$VELERO_LAST_BACKUP_NAME" ]] && f_error "No Velero backups found after waiting" echo "Found backup: ${VELERO_LAST_BACKUP_NAME}" echo "" echo "----- Get All Velero Backups --------------------------------------------------" velero get backup echo "*** Create Velero restore for cluster ${CLUSTER_NAME} from backup ${VELERO_LAST_BACKUP_NAME} ..." velero restore create "${CLUSTER_NAME}-restore-$(date +%Y%m%d-%H%M)" \ --from-backup "$VELERO_LAST_BACKUP_NAME" \ --include-cluster-resources=true \ --exclude-namespaces kube-system,kube-public,kube-node-lease,velero \ --wait || f_error "Failed to create Velero restore" fi ###### OPTIN: costs ################################################################################################## if [[ "$1" == "init" || "$1" == "costs" ]]; then IP_CLUSTER_MASTER=$(hcloud server describe "${CLUSTER_NAME}"-master1 -o format='{{.PublicNet.IPv4.IP}}') echo "Kubernetes $CLUSTER_NAME Master IP address: $IP_CLUSTER_MASTER" echo "" PRICE_PER_GB_MO_NET="0.044" { printf "%-8s %-10s %-42s %-12s %5s %7s %-6s %10s %10s\n" \ "TYPE" "ID" "NAME" "MODEL" "CPU" "RAM_GB" "SITE" "PRICE/h" "PRICE/MO" { jq -s -r ' .[0] as $servers | .[1] as $types | $servers[] as $s | ($s.datacenter.location.name // $s.location.name) as $loc | ($types[] | select(.id == $s.server_type.id)) as $t | ($t.prices[] | select(.location == $loc)) as $p | [ "SERVER", $s.id, $s.name, $t.name, $t.cores, $t.memory, $loc, $p.price_hourly.net, $p.price_monthly.net ] | @tsv ' \ edok3s-backup-20260513-0943 Completed 0 0 2026-05-13 09:43:33 +0200 CEST 182d default edok3s-backup-20260513-1019 Completed 0 0 2026-05-13 10:19:53 +0200 CEST 182d default edok3s-backup-20260513-1107 Completed 0 0 2026-05-13 11:07:37 +0200 CEST 182d default edok3s-backup-20260513-1311 Completed 0 0 2026-05-13 13:11:18 +0200 CEST 182d default *** Create Velero restore for cluster edok3s from backup edok3s-backup-20260513-1311 ... Restore request "edok3s-restore-20260513-1316" submitted successfully. Waiting for restore to complete. You may safely press ctrl-c to stop waiting - your restore will continue in the background. ...................... Restore completed with status: Completed. You may check for more information using the commands `velero restore describe edok3s-restore-20260513-1316` and `velero restore logs edok3s-restore-20260513-1316`. Enter fullscreen mode Exit fullscreen mode Hetzner server list after 'restore' Hetzner volume list after 'restore' 🔹Costs 1. Run ./init_cluster.sh cost Enter fullscreen mode Exit fullscreen mode In EUROS [€] Kubernetes edok3s Master IP address: 178.105.69.141 TYPE ID NAME MODEL CPU RAM_GB SITE PRICE/h PRICE/MO SERVER 130781232 edok3s-master1 cpx22 2 4 fsn1 0.0128 7.99 VOLUME 105698396 pvc-f0278cda-ba7a-422c-859f-054004afda6a 10GB - 10 fsn1 0.0006 0.44 VOLUME 105698397 pvc-4880c913-33be-40a0-a2c2-9ebb5951ad08 10GB - 10 fsn1 0.0006 0.44 VOLUME 105698398 pvc-30aa4c80-9c6e-40f8-99d9-5f824cc21a1a 20GB - 20 fsn1 0.0012 0.88 Enter fullscreen mode Exit fullscreen mode

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.