Gendi Notes

Building a Cluster with kops

kops treats a cluster as a declarative object stored in S3 — you edit the spec, then tell it to make reality match, which is exactly the Kubernetes model applied to the cluster itself.

Level 3 runbook kuberneteskopsawscluster

What is it?#

kops (Kubernetes Operations) creates and manages self-managed Kubernetes clusters on AWS: it provisions the VMs, runs the control plane on machines you own, and manages upgrades.

YOU STATE STORE (S3) AWS ─── ──────────────── ─── kops create → cluster spec → ASGs, EC2, ELB, kops edit → (YAML, versioned) Route53, EBS, IAM, kops update → security groups │ kops validate ←───────────┴─────────────→ control plane runs on desired vs actual YOUR instances

Before you start#

RequirementWhyCheck
An AWS account with adminkops creates IAM, VPC, EC2, Route53aws sts get-caller-identity
A registered domain, or a Route53 hosted zonekops publishes the API endpoint in DNSaws route53 list-hosted-zones
kubectlTo use the cluster afterwardskubectl version --client
An SSH key pairkops installs the public key on every nodels ~/.ssh/id_ed25519.pub
Install the tools
# kops — check the current release rather than copying a version from a blog
curl -Lo kops https://github.com/kubernetes/kops/releases/latest/download/kops-linux-amd64
chmod +x kops && sudo mv kops /usr/local/bin/

kops version
aws --version

Step 1 — the state store#

kops keeps the cluster spec in an S3 bucket. This bucket is your cluster definition: lose it and you cannot manage the cluster any more.

export NAME=k8s.example.com
export KOPS_STATE_STORE=s3://kops-state-example-com

aws s3api create-bucket \
  --bucket kops-state-example-com \
  --region eu-west-1 \
  --create-bucket-configuration LocationConstraint=eu-west-1

# Versioning is NOT optional — it is how you recover a bad spec change
aws s3api put-bucket-versioning \
  --bucket kops-state-example-com \
  --versioning-configuration Status=Enabled

aws s3api put-bucket-encryption \
  --bucket kops-state-example-com \
  --server-side-encryption-configuration \
  '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'

# Block public access. The spec contains cluster topology and CA material.
aws s3api put-public-access-block \
  --bucket kops-state-example-com \
  --public-access-block-configuration \
  "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

Step 2 — create the cluster spec#

kops create cluster writes the spec. It does not build anything yet.

kops create cluster \
  --name=${NAME} \
  --state=${KOPS_STATE_STORE} \
  --zones=eu-west-1a,eu-west-1b,eu-west-1c \
  --control-plane-count=3 \
  --control-plane-size=t3.medium \
  --node-count=3 \
  --node-size=t3.large \
  --networking=cilium \
  --topology=private \
  --bastion \
  --ssh-public-key=~/.ssh/id_ed25519.pub \
  --kubernetes-version=1.31.4 \
  --dry-run -o yaml > cluster.yaml
FlagWhy it matters
--zones with three AZsetcd needs a quorum; two AZs cannot survive one failing
--control-plane-count=3One is a single point of failure. Use an odd number
--networking=ciliumThe CNI. Cilium/Calico give you NetworkPolicy; the default may not
--topology=privateNodes get no public IPs. Strongly preferred
--bastionWith private topology you need a jump host for SSH
--kubernetes-versionPin it. Do not inherit whatever is newest
--dry-run -o yamlReview before creating. Commit this file to Git
# Review it, then commit it, THEN create
less cluster.yaml
git add cluster.yaml && git commit -m "kops: initial cluster spec"

kops create -f cluster.yaml --state=${KOPS_STATE_STORE}
kops create secret --name ${NAME} sshpublickey admin -i ~/.ssh/id_ed25519.pub

Step 3 — build it#

# What WOULD be created — read this, it is a long list
kops update cluster --name ${NAME} --state=${KOPS_STATE_STORE}

# Actually build it, and write a kubeconfig entry
kops update cluster --name ${NAME} --state=${KOPS_STATE_STORE} --yes --admin

kops now creates, roughly in order:

IAM roles and instance profiles ↓ VPC · subnets (public + private) · NAT gateways · route tables ↓ Security groups ↓ Route53 records: api.k8s.example.com ↓ Launch templates + Auto Scaling Groups ↓ ASGs boot instances; each runs nodeup on first boot Control-plane instances: etcd, apiserver, scheduler, controller-manager ↓ Worker instances: kubelet joins via the API endpoint from DNS ↓ Addons: CNI, CoreDNS, and whatever the spec declares

Then wait — this takes 5–15 minutes:

kops validate cluster --name ${NAME} --state=${KOPS_STATE_STORE} --wait 15m
kubectl get nodes -o wide

Step 4 — day-two operations#

Changing the cluster#

Everything is an edit to the spec followed by an update. Same loop, always:

kops edit cluster ──→ changes the spec in S3 kops edit ig nodes ──→ changes an instance group │ ↓ kops update cluster --yes ──→ changes AWS resources │ ↓ kops rolling-update cluster --yes ──→ replaces instances, one at a time (only if instances must change)
# Scale the workers: edit minSize/maxSize
kops edit ig --name ${NAME} nodes

kops update cluster --name ${NAME} --yes
# Scaling an ASG needs no rolling update; changing the AMI or instance type does

Upgrading Kubernetes#

# 1. See what kops proposes
kops upgrade cluster --name ${NAME}

# 2. Pin the version deliberately instead
kops edit cluster --name ${NAME}      # set spec.kubernetesVersion: 1.32.1

# 3. Apply to the spec, then roll the instances
kops update cluster --name ${NAME} --yes
kops rolling-update cluster --name ${NAME} --yes \
  --control-plane-interval=5m --node-interval=3m

Deleting#

kops delete cluster --name ${NAME} --state=${KOPS_STATE_STORE}         # preview
kops delete cluster --name ${NAME} --state=${KOPS_STATE_STORE} --yes   # do it

What goes wrong#

SymptomCauseFix
validate fails: nodes not joiningNodes cannot resolve or reach api.<name>Check the Route53 record and security groups
Stuck at 1 of 3 control-plane nodesetcd has no quorumkops get instances; check etcd logs on the running node
error reading s3://...Wrong KOPS_STATE_STORE, or no IAM permissionaws s3 ls ${KOPS_STATE_STORE}
InsufficientInstanceCapacityThe AZ has no capacity for that typeAnother instance type or AZ
Nodes NotReady, pods ContainerCreatingCNI addon not healthykubectl -n kube-system get pods
kubectl works, then stops after a whileThe admin credential from --admin is short-livedkops export kubecfg --admin again
Rolling update hangs on one nodeA PodDisruptionBudget blocks drainingkubectl get pdb -A; fix or --cloudonly deliberately
Cluster works, kops validate failsSpec drift from a manual console changekops update --yes to reassert the spec
Where to look when a node does not join
kops get instances --name ${NAME}
ssh -J ubuntu@bastion.${NAME} ubuntu@<node-private-ip>

sudo journalctl -u kops-configuration -n 100   # nodeup, the first-boot agent
sudo journalctl -u kubelet -n 100

Interview corner · الأسئلة اللي بتتسأل#

Why does a control plane need an odd number of nodes?

Because etcd uses Raft, and Raft requires a majority (a quorum) to commit a write. With N members the quorum is floor(N/2) + 1.

MembersQuorumFailures tolerated
110
220
321
431
532

Two key observations:

  • Two is strictly worse than one. Same zero tolerance, twice the hardware that can fail — so the probability of an outage doubles for no benefit.
  • Four is no better than three. Both tolerate one failure, but four costs more and makes every write wait on a larger quorum.

What happens when quorum is lost: etcd goes read-only. The API server can still serve reads, so kubectl get works — but no writes succeed, so no deploys, no scaling, no rescheduling. Running pods keep running, which makes the failure quiet and easy to misdiagnose.

kops or EKS? · نستخدم kops ولا EKS؟

Ask what constraint forces the choice, then answer.

kopsEKS
Control planeYours — your instances, your problemAWS runs it, per-hour fee
UpgradesYou run them, one minor at a timeAWS-assisted, still your responsibility for nodes
etcd backupsYours to verifyAWS manages
CustomisationFull — any flag, any componentLimited to what EKS exposes
Time to first cluster15 minutes plus DNS setupMinutes, or one Terraform module
Ongoing costEC2 onlyEC2 plus the control-plane fee
Who to call at 3amYouAWS support

Choose EKS by default. The control-plane fee is far less than the salary cost of operating etcd yourself, and "we run our own control plane" is undifferentiated work.

Choose kops when you need control EKS does not expose, you are not on AWS in the same account model, or you have a regulatory reason to own every component.

You lose the S3 state store. What can you still do?

Almost nothing, from kops' point of view. The cluster keeps running — the control plane and kubelets do not consult S3 — but you have lost the ability to manage it: no kops update, no kops rolling-update, no upgrades, no scaling through kops.

You also lose the cluster CA material kops keeps there, and the etcd backups it writes there.

Recovery options, worst to best:

  1. S3 versioning enabled → restore the deleted objects. This is why versioning is step one.
  2. Reconstruct the spec by hand from the running cluster and AWS resources — possible, tedious, and error-prone.
  3. Build a new cluster and migrate workloads.

The lesson being tested is that the state store is a critical, stateful dependency of your tooling, exactly like Terraform state — and that it needs versioning, encryption, restricted IAM and a tested restore, decided before the first kops create.

Key takeaways#

  • kops applies the Kubernetes model to the cluster itself: edit a spec in S3, then reconcile AWS to match it.
  • The state store is as sensitive as the cluster. Versioning, encryption and tight IAM, before the first create.
  • kops create cluster only writes the spec. kops update --yes builds, and kops validate is the real completion signal.
  • Odd control-plane counts, three AZs. Two nodes are worse than one.
  • Upgrade one minor version at a time, with a verified etcd backup first.
  • Delete leaves orphans — LoadBalancers and PVC volumes outlive the cluster.
  • Default to EKS. Self-managing a control plane needs a reason you can name.

Last updated 2026-09-13