Skip to content

Notes10-Cluster Design

Cluster Design

How many control planes, where etcd sits, what you run yourself versus what a managed service runs, and how Helm, Kustomize, CRDs, and operators extend a cluster.

Updated Oct 1, 2026

Cluster Design Quick Notes





What you are sizing

Piece

Sensitive to

Practical note

etcd

Disk latency and a stable majority

SSD, dedicated or at least isolated from image pulls. Odd member count

kube-apiserver

Request rate (watchers, controllers, CI)

Horizontally scaled behind one address

Workers

CPU, memory, pods-per-node, disk for images

The schedulable capacity. Control planes are tainted so they are not this

Network

Pod CIDR size and node count

A /16 Pod CIDR with a /24 per node is about 250 nodes. Plan the math before kubeadm init

The Pod CIDR cannot be renamed casually after init. Service CIDR neither. Pick them so they do not overlap the VPC, the office, or each other.

Managed and self-managed

Approach

Control plane

You still own

Managed (EKS, GKE, AKS, and similar)

Vendor runs apiserver and etcd

Node pools, RBAC, workloads, often the CNI choice

kubeadm

You run it on VMs you patch

kubeadm init and kubeadm join, the CNI addon, OS patches, and etcd snapshots (etcdctl snapshot save before an upgrade)

Hosted installer (kOps, Cluster API)

Generated and reconciled for you

The installer’s CRDs and the underlying machines

With kubeadm you bring the machines. kubeadm init writes certificates, static Pod manifests under /etc/kubernetes/manifests, and admin.conf. Nodes stay NotReady until you install a CNI whose Pod CIDR matches --pod-network-cidr. Workers join with kubeadm join and a short-lived token. You still choose how many control planes, whether etcd is stacked or external, and which addon provides CNI, CSI, and Ingress.


High availability

One control plane is a single failure domain. HA means:

  • At least three control-plane nodes (or three stacked etcd members).
  • One stable address in front of the apiservers: a TCP load balancer or a DNS name with a short TTL. kubeadm calls this --control-plane-endpoint (for example cp.example.com:6443). Every kubeconfig and every join uses that name, not a single node’s private IP.
  • Loss of one member leaves the cluster able to elect an etcd leader and serve API writes.

Workers are already multiple. Adding workers does not make the control plane highly available.

kubeadm taints control-plane nodes node-role.kubernetes.io/control-plane:NoSchedule. Leave the taint on so workloads stay on workers. Workloads on the control plane compete with etcd for disk and CPU. Remove it only when a single node must run both.


Stacked etcd and external etcd

Stacked: etcd runs as a static Pod on each control-plane node, next to the apiserver. kubeadm’s default.

  • Fewer machines (three nodes can be a complete HA control plane).
  • A node loss removes an apiserver and an etcd member together.
  • Couples etcd disk IO to whatever else that node does.

External: etcd runs on a separate set of machines. Control planes only run apiserver, scheduler, and controller-manager, and point --etcd-servers at the etcd cluster.

  • etcd can be sized and snapshotted on its own.
  • You operate two sets of machines and two failure domains.
  • Member count is still odd and independent of the apiserver count (three etcd, two or more apiservers is a valid layout).



Stacked

External

Where etcd runs

Static Pod on the control plane

Dedicated hosts

Minimum HA footprint

3 control-plane nodes

3 etcd + at least 2 control planes

Failure coupling

Node loss hits API and etcd together

etcd failure is independent of an apiserver process crash

kubeadm

Default kubeadm init

kubeadm init with an external etcd block in the config


etcd quorum

etcd commits a write when a majority of members agree.

Members

Majority

Failures tolerated

1

1

0

3

2

1

5

3

2

An even count (2 or 4) does not tolerate more failures than the odd count below it, and it costs another machine. Add members one at a time so the cluster always has a leader. Replacing a member is etcdctl member add / member remove, then a new peer with the matching --initial-cluster data.


A single-member snapshot and restore, run on the control plane with the etcd client certs in /etc/kubernetes/pki/etcd/:



bash
sudo ETCDCTL_API=3 etcdctl snapshot save /opt/etcd-backup.db \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key

sudo ETCDCTL_API=3 etcdctl snapshot restore /opt/etcd-backup.db \
  --data-dir=/var/lib/etcd-restore


snapshot restore does not overwrite the live directory. Edit /etc/kubernetes/manifests/etcd.yaml so the data volume hostPath and --data-dir use /var/lib/etcd-restore. The kubelet restarts the etcd static Pod. The apiserver stays down until etcd answers again.


Extension interfaces

Kubernetes stays small by calling out:

Interface

What it adds

CRI

Container runtime. The kubelet starts containers through containerd or CRI-O. crictl ps shows them

CNI

Pod IPs and routes. Config in /etc/cni/net.d. Calico and Cilium also enforce NetworkPolicy. Flannel does not

CSI

Volumes. A driver’s StorageClass provisioner creates disks when a PVC asks. The kubelet mounts the result

CRD

A new kind in the API. After the CustomResourceDefinition is established, kubectl get <plural> works like a built-in

Device plugin / DRA

Extra hardware, such as GPUs, advertised to the scheduler

The core binaries do not contain Calico, EBS, or ingress-nginx. Those are addons you install after kubeadm init.


CRDs and operators

A CustomResourceDefinition adds a kind to the API (certificates.cert-manager.io, grafanaagents.grafana.com). After the CRD is established, kubectl get <plural> works like any built-in.


bash
kubectl get crd
kubectl explain certificate --api-version=cert-manager.io/v1


An operator is a controller that watches those objects and reconciles something outside the object (issue a certificate, deploy Prometheus, resize a database). Installing an operator is usually “apply a CRD, apply a Deployment, then create a custom resource.” If the CRD is missing, the custom resource returns “no matches for kind”. If the controller Deployment is down, the object exists and its status never becomes Ready.


Helm

Helm installs a chart (templated manifests) as a named release.


bash
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx --create-namespace
helm list -A
helm upgrade ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx
helm uninstall ingress-nginx -n ingress-nginx
helm get manifest ingress-nginx -n ingress-nginx


  • Release name is how you upgrade and delete. The generated object names usually include it.
  • Values (-f values.yaml or --set key=value) are the supported way to change a chart. Editing a live object by hand is overwritten on the next helm upgrade.
  • Namespace is per release. helm list without -A hides other namespaces.
  • Charts vendor their own CRDs. Install order problems (“no matches for kind”) usually mean the CRDs have not been applied yet. helm install applies CRDs from the chart’s crds/ directory first. helm upgrade does not upgrade those CRDs; apply CRD changes on purpose.


Kustomize

Kustomize patches plain YAML without a templating language. kubectl has it built in.


yaml
kustomization.yaml
resources:
  - deployment.yaml
  - service.yaml
commonLabels:
  app: web
images:
  - name: nginx
    newTag: "1.27"



bash
kubectl apply -k ./overlays/prod
kubectl kustomize ./overlays/prod | less


  • resources: Files or directories to include.
  • commonLabels / labels: Added to every object and, where it matters, to selectors. Changing a selector later orphans Pods. Prefer a label that is stable.
  • images.newName / newTag: Rewrite image references at apply time.
  • patches: Strategic merge or JSON patch against one object.
  • Bases and overlays: overlays/prod has a kustomization.yaml whose resources point at ../../base. Prod and dev differ by patches, not by forked copies of the Deployment.

kubectl apply -f on a directory does not run Kustomize. The flag is -k.


Choosing an install tool

You are given

Do this

A raw manifest URL

kubectl apply -f

A directory with kustomization.yaml

kubectl apply -k

A chart name and a repo URL

helm install

“Change the image tag in this overlay”

Edit kustomization.yaml newTag, then kubectl apply -k

Official docs