Right, so you want a production-grade Kubernetes cluster. Not a toy for your laptop, but the real deal. That means we’re reaching for kubeadm. It’s the official, blessed, “I know what I’m doing” tool for bootstrapping a cluster. It’s not magic; it’s a glorified orchestration tool that runs a series of checks and creates all the necessary components—the API server, etcd, scheduler, the whole gang—as static pods. This is brilliant because it means Kubernetes itself manages its own control plane. If the API server crashes, the kubelet restarts it. Poetic, really.

But let’s be clear: kubeadm init is the beginning of the journey, not the end. Anyone who tells you to just run that and walk away has a cluster running on hope and default settings. We’re not doing that.

The Pre-Flight Checklist (Or, How to Avoid Your Own Hubris)

Before we even think about running kubeadm init, we have to prepare the ground. The kubeadm pre-flight checks are good, but they’re not omniscient. Here’s what you absolutely must do on every node (control plane and workers):

  1. Cripple the Firewall: Or, more accurately, tell it to play nice. You need to allow the correct ports for the control plane nodes (TCP 6443, 2379-2380, 10250, 10259, 10257) and the workers. The iptables tool must see bridged traffic. This is non-negotiable.

    # Enable iptables to see bridged traffic on all nodes
    cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
    br_netfilter
    EOF
    
    cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
    net.bridge.bridge-nf-call-ip6tables = 1
    net.bridge.bridge-nf-call-iptables = 1
    net.ipv4.ip_forward = 1
    EOF
    sudo sysctl --system
    
  2. Murder swap. This isn’t a suggestion. The kubelet will straight-up refuse to run if swap is enabled. It’s a design choice to ensure predictability (swap kills performance in a noisy way). So, disable it.

    # Disable swap immediately
    sudo swapoff -a
    # Comment out any swap lines in /etc/fstab to make it permanent
    sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
    
  3. Install the Holy Trinity: You need kubeadm, kubelet, and kubectl on all nodes. Use the official Kubernetes repos. Don’t just apt-get install from your distro’s default repo; you’ll get geriatric versions.

The kubeadm init That Actually Works

Now for the main event. We’re not running it naked. We’re going to generate a configuration file because we need to change things. The default settings are, frankly, for amateurs. The most important thing? Setting the controlPlaneEndpoint to a stable DNS name or load balancer before you initialize. This is your single biggest “future-proofing” step. It allows you to add more control plane nodes later for high availability. If you just use the node’s IP, you’re locked into a single control plane node forever. Don’t do that.

# Generate a default config file to work from
kubeadm config print init-defaults > kubeadm-init.yaml

Now, crack open kubeadm-init.yaml. You’ll need to change at least these things:

  • localAPIEndpoint.advertiseAddress: The IP address the API server will advertise.
  • controlPlaneEndpoint: Your stable DNS name or load balancer IP (e.g., my-k8s-api.example.com:6443).
  • nodeRegistration.name: Change from node to your actual node’s hostname.
  • Under apiServer, controllerManager, and scheduler, you’ll find extraArgs. This is where you set critical flags. A big one: apiServer.extraArgs.service-account-issuer and service-account-jwks-uri for proper OIDC discovery if you ever want to use service account tokens outside the cluster.

Once your config file is polished, run the pre-flight checks with it:

sudo kubeadm init --config kubeadm-init.yaml --dry-run

If that looks good, take a deep breath and run it for real:

sudo kubeadm init --config kubeadm-init.yaml --upload-certs

The --upload-certs flag is a lifesaver if you plan to add other control plane nodes later; it automatically uploads and shares the certificates.

The Aftermath and Joining the Party

Congratulations, your control plane is running. Now, kubeadm will spit out two crucial commands. Copy them to a text file immediately. The first is your standard kubectl setup:

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

The second is the kubeadm join command with a token and CA certificate hash. This is what your worker nodes (and future control plane nodes) will run to join the cluster. This token expires after 24 hours. If you need a new one later, generate it with kubeadm token create --print-join-command.

The Most Common “Oh, Come On” Moment

You’ve joined your nodes, but when you run kubectl get nodes, your worker nodes are stuck in NotReady state. 99% of the time, this is because you forgot to install a Container Network Interface (CNI) plugin. Kubernetes is a diva; it won’t consider a node ready until a network pod is running. Let’s fix that with the Canal plugin (which combines Flannel for networking and Calico for network policy):

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

Wait a minute, then watch your nodes flip to Ready. See? Not so bad. You’ve now got a cluster that’s actually built to last, not just to start.