Skip to main content
RunBook Academy

KubernetesLXXIV · kubeadmkubeadm

kubeadm join — control-plane and worker node bootstrapping

Advanced⏱ ~17 minkubeadmkubectl

What you'll learn

  • Run kubeadm join for worker and control-plane nodes
  • Trace the join process (bootstrap, CSR, kubelet)
  • Reason about node labelling and taints
  • Plan cluster growth with --control-plane

Prerequisites

Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16

Not yet marked complete on this device.

kubeadm join adds a node to an existing cluster. For a worker, the bootstrap is straightforward; for a control-plane, the join also establishes an etcd member and an API server on the new host. This lesson walks both flows and the production discipline.

The join command

After kubeadm init on the first control-plane host:

kubeadm join loadbalancer.example:6443 \
  --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:abc...

This is the join command for both worker and additional control-plane hosts. The flags:

FlagPurpose
loadbalancer.example:6443The cluster’s API server endpoint
--tokenBootstrap token issued at init time (24h TTL)
--discovery-token-ca-cert-hashThe API server’s CA cert hash

The worker join

On a worker host:

sudo kubeadm join loadbalancer.example:6443 \
  --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:abc...

This command:

  1. Installs the kubelet binary (already there typically).
  2. Reads the bootstrap token.
  3. Connects to the cluster’s API server.
  4. Submits a CSR (CertificateSigningRequest) for the node’s identity.
  5. Once approved (auto-approved for nodes), installs the kubelet’s cert.
  6. Configures the kubelet with the cluster info.
sequenceDiagram
    autonumber
    participant N as New node
    participant AS as API server
    participant KC as kube-controller-manager
    participant K as kubelet on new node
    N->>AS: GET /api/v1/nodes (use bootstrap token)
    AS-->>N: cluster info (CA, endpoint)
    N->>AS: POST CSR (system:node:<node-name>)
    AS-->>KC: watch CSR
    KC->>KC: auto-approve (nodes have CSR auto-approval)
    KC->>AS: approve CSR
    N->>AS: GET approved cert
    N->>N: install client cert for kubelet
    K->>AS: register Node (heartbeat)
    K->>K: start running Pods (when scheduled)

The new node is now a worker in the cluster.

The control-plane join

For an additional control-plane host:

sudo kubeadm join loadbalancer.example:6443 \
  --token abcdef.0123456789abcdef \
  --discovery-token-ca-cert-hash sha256:abc... \
  --control-plane \
  --certificate-key abcdef1234567890...

The --control-plane flag triggers additional steps:

  1. Install etcd member on the new host.
  2. Add the etcd member to the cluster.
  3. Install kube-apiserver static pod.
  4. Install kube-controller-manager static pod.
  5. Install kube-scheduler static pod.
sequenceDiagram
    autonumber
    participant N as New control-plane host
    participant AS as API server (existing)
    participant E as etcd cluster
    N->>AS: bootstrap token
    N->>N: install etcd member
    N->>AS: etcdctl member add
    N->>N: install kube-apiserver static pod
    N->>N: install kube-controller-manager static pod
    N->>N: install kube-scheduler static pod
    N->>AS: register Node (with control-plane label)

The new host becomes a control-plane node.

The node labelling

After join, the node is labelled:

kubectl get nodes --show-labels
NAME    STATUS   ROLES           LABELS
cp-1    Ready    control-plane   <control-plane labels>
cp-2    Ready    control-plane   <control-plane labels>
cp-3    Ready    control-plane   <control-plane labels>
worker-1 Ready   <none>          <default labels>

Control-plane nodes have taints (no Pod placement by default):

kubectl describe node cp-1 | grep Taints
# Taints: node-role.kubernetes.io/control-plane:NoSchedule

To run control-plane Pods on the node, set --register-with-taints differently at init / join, or remove the taint manually.

What the join does not do

  • Install the CNI plugin (presumably already done).
  • Install add-ons (ingress, cert-manager, etc.).
  • Configure the kubelet’s runtime (presumably preset).
  • Schedule Pods (the cluster scheduler runs after the kubelet registers).

The join assumes the kubelet and runtime are installed before running.

The post-join sanity

# Substitute your own value before running:
NODE=cp-02   # the node name as it appears in `kubectl get nodes`

# 1. The node is listed
kubectl get nodes

# 2. The node reaches Ready
kubectl wait node/"$NODE" --for=condition=Ready --timeout=60s

# 3. (control-plane) etcd has the new member
kubectl -n kube-system exec "etcd-$NODE" -- etcdctl member list

# 4. (control-plane) The API server is running
kubectl -n kube-system get pod -l component=kube-apiserver
Read-only / Safe
$ kubectl get nodes -o wide
...

Common join issues

IssueCauseFix
Token expiredThe bootstrap token TTL elapsedRun kubeadm token create --print-join-command
Hash mismatchThe CA cert hash is wrongVerify against the original init output
Network unreachable to API serverLB / firewallVerify connectivity from new node
Cert approval failedRBAC deniedCheck the controller-manager logs
kubelet fails to registerVersions mismatchVerify kubelet version matches API server

The bootstrap token rotation

For long-running clusters, the bootstrap token expires:

# After 24 hours:
sudo kubeadm token create --print-join-command

The token is reusable while it has TTL; the printed join command is for workers.

The multi-control-plane join sequence

For a 5-control-plane production cluster:

# Values from `kubeadm token create --print-join-command` and
# `kubeadm init phase upload-certs --upload-certs`:
JOIN_TOKEN=9x7k2f.7bd1a9c0e4f6238a
CA_CERT_HASH=1f3c8a7e5b0d94f26a1c7e3b8d05f4a29c6e1b7d3f80a5c2e9b4d6f1a8c3e5b7
CERT_KEY=b41d7e6c2a95f803d1e7b4a6c90f52d8b3e7a1c46f9027b5d8e3a1c6f4b20e94

# On each cp-2, cp-3, cp-4, cp-5:
sudo kubeadm join loadbalancer.example:6443 \
  --token "$JOIN_TOKEN" \
  --discovery-token-ca-cert-hash "sha256:$CA_CERT_HASH" \
  --control-plane \
  --certificate-key "$CERT_KEY"

Each control-plane join runs in parallel (with care not to overload the network).

The certificate-key rotation

The certificate-key is set at init time. To rotate:

# Generate a fresh key with `kubeadm certs certificate-key`, then:
NEW_CERT_KEY=6d2f9b81c47e0a3f5b8d16c2e94a7f03b5c8e1d47a2f960b3e8c5d1a7f4b2e60

sudo kubeadm init phase upload-certs --upload-certs --certificate-key="$NEW_CERT_KEY"

This invalidates old certificate keys.

The discipline of join

  • Capture the join command immediately. It is only printed once (at init).
  • Issue new tokens as needed. Tokens expire.
  • Verify post-join. All nodes Ready, etcd member count, control-plane pods running.
  • Document the cluster’s node map. Each node’s role, AZ, IP, install date.
  • Restrict who can join. The bootstrap token is a cluster-issuing credential; rotate after the initial join.

Quiz

Knowledge check · 4 questions

  1. Q1. What does kubeadm join do differently for control-plane vs worker nodes?

  2. Q2. The bootstrap token issued by `kubeadm init` is valid forever.

  3. Q3. kubeadm join fails with 'token expired'. Walk the remediation.

    Cluster was initialised 2 days ago. Today the team tries to join a new control-plane host; the join fails because the original token has expired.

  4. Q4. What is the difference between worker and control-plane bootstrap, and why does it matter?

Passing score: 75%. Answers are checked in this browser.

Production discipline

  • Capture the join command. It’s only printed once; store it for the team’s reference.
  • Issue fresh tokens as needed. Tokens expire; rotate the procedure.
  • Verify post-join. Nodes Ready, etcd member count, control-plane pods running.
  • Document the node map. Each node’s role, AZ, IP, installation date.
  • Restrict join access. Tokens are cluster-issuing credentials; treat them carefully.

kubeadm join is the cluster’s growth mechanism. Operating it well is keeping the cluster’s growth controlled and verified.