Introduction
KubeOne is Kubermatic’s open-source tool for automating the full lifecycle of Kubernetes clusters. In this tutorial, you’ll go from bare metal servers to a production-ready cluster.
Step 1: Install KubeOne
Download the latest KubeOne binary for your platform:
curl -sfL https://get.kubeone.io | sh
kubeone version
Step 2: Prepare Your Infrastructure
On bare metal, you skip Terraform entirely — your servers already exist. You do, however, need one piece of infrastructure in front of the control plane: a load balancer. This is non-negotiable for HA, because kubectl and every worker node need a single stable address to talk to, not three separate control plane IPs.
The simplest open-source option is HAProxy on a separate small VM (or two, with keepalived for the VIP) forwarding TCP 6443 to all three control plane nodes. For this tutorial we’ll assume your load balancer is reachable at api.production-edge.example.com.
The HA topology looks like this:
flowchart TD
Client["kubectl / workers"]
LB["Load balancer
api.production-edge.example.com:6443"]
Client --> LB
LB --> CP1["control-plane-1
203.0.113.10
kube-apiserver + etcd"]
LB --> CP2["control-plane-2
203.0.113.11
kube-apiserver + etcd"]
LB --> CP3["control-plane-3
203.0.113.12
kube-apiserver + etcd"]
CP1 <-.->|etcd raft| CP2
CP2 <-.->|etcd raft| CP3
CP1 <-.->|etcd raft| CP3
Warning: Ensure all servers can communicate with each other on the private network. Firewall rules must allow Kubernetes API (6443), etcd (2379-2380), and kubelet (10250) traffic between the control plane nodes, plus 6443 from the load balancer to each control plane node.
Step 3: Create the KubeOne Configuration
apiVersion: kubeone.k8c.io/v1beta2
kind: KubeOneCluster
name: production-edge
versions:
kubernetes: "v1.30.2"
cloudProvider:
none: {}
apiEndpoint:
host: "api.production-edge.example.com"
port: 6443
controlPlane:
hosts:
- publicAddress: "203.0.113.10"
privateAddress: "10.0.0.10"
sshUser: "ubuntu"
- publicAddress: "203.0.113.11"
privateAddress: "10.0.0.11"
sshUser: "ubuntu"
- publicAddress: "203.0.113.12"
privateAddress: "10.0.0.12"
sshUser: "ubuntu"
The apiEndpoint block is what makes this cluster actually HA. KubeOne writes that hostname into the generated kubeconfig and into every worker node’s kubelet config, so every API call hits the load balancer and can reach any healthy control plane node. Omit it and you’ll get three control plane nodes that nobody can failover between.
Step 4: Provision the Cluster
kubeone apply --manifest kubeone.yaml
KubeOne will:
- Install container runtime (containerd)
- Bootstrap the first control plane node
- Join remaining control plane nodes
- Configure networking (Canal CNI by default)
- Deploy machine-controller for worker nodes
Step 5: Verify Your Cluster
export KUBECONFIG=$PWD/production-edge-kubeconfig
kubectl get nodes
kubectl get pods -A
You should see all three control plane nodes in Ready state.
Next Steps
- Add worker nodes using MachineDeployments
- Configure persistent storage with a CSI driver
- Set up monitoring with Prometheus and Grafana
- Enable cluster autoscaling
Summary
You’ve successfully provisioned a highly available Kubernetes cluster on bare metal using KubeOne. The cluster is production-ready with three control plane nodes for fault tolerance.
