What to install in a fresh cluster
metrics-server for kubectl top, the status of the archived ingress-nginx, Gateway API, and StorageClass.
The cluster came up, the nodes are Ready, kubectl responds. Then it
turns out kubectl top pod complains about a missing Metrics API,
Ingress objects get created but lead nowhere, and a
PersistentVolumeClaim sits in Pending. Nothing is broken: these
components simply aren't in the cluster, or aren't everywhere.
Why
The local clusters from earlier lessons differ in more than how they're started. One ships metrics-server and an Ingress controller out of the box, another needs them installed, a third enables them as addons. A tutorial written for k3d will hit Pending on kind, and the other way around. If you know what's missing and how to check, these errors take a minute to sort out, not an evening.
This lesson covers the four things most often missing: metrics-server, an Ingress controller, Gateway API and a StorageClass.
| Tool | metrics-server | Ingress | StorageClass |
|---|---|---|---|
| kind | installed and patched by hand | native via cloud-provider-kind v0.9.0+ | standard |
| k3s and k3d | already in the cluster | Traefik | local-path |
| minikube | addon, no patching needed | addon, ingress-nginx 1.15.1 (minikube v1.39.0) | standard |
| MicroK8s | addon | addon, nginx-based | addon microk8s-hostpath |
metrics-server
kubectl top node and kubectl top pod don't work on their own. Behind
them stands the Metrics API, implemented by a separate component,
metrics-server. It's also what feeds numbers to the
HorizontalPodAutoscaler.
On a fresh kind cluster it looks like this:
kubectl top node
error: Metrics API not available
The current version is v0.9.0 from 2026-07-13, image
registry.k8s.io/metrics-server/metrics-server:v0.9.0. The manifest
lives in the project's releases, grab the link with the version number:
https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.9.0/components.yaml
Why a local cluster needs --kubelet-insecure-tls
metrics-server fetches metrics from the kubelet over HTTPS and checks
its certificate. In kind, k3d and minikube the kubelet serves a
self-signed serving certificate that fails x509 verification. The
metrics-server Pods never reach Ready, and kubectl top answers
"Metrics API not available".
The fix is the --kubelet-insecure-tls flag, which turns off that
check. The default manifest doesn't have this flag, and that's by
design: the manifest targets a normal cluster, where a disabled
certificate check on the kubelet would be a hole. The project's README
describes the flag as "For testing purposes only".
For kind, install the manifest and then patch it:
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.9.0/components.yaml
kubectl -n kube-system patch deployment metrics-server --type=json \
-p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
Where you don't need to install it. In k3s and k3d, metrics-server is
already part of the packaged components, alongside coredns, traefik,
local-storage and servicelb. Disable it with --disable=metrics-server.
Where it's turned on as an addon:
minikube addons enable metrics-server # built with the right flags already, no patching needed
microk8s enable metrics-server
Ingress: ingress-nginx is archived
The most popular Ingress controller in Kubernetes is no longer developed. You need to know this before you open any two-year-old tutorial.
The kubernetes/ingress-nginx repository was archived on March 24,
2026 and made read-only. The README says it plainly: "If you are not
already using ingress-nginx, you should not be deploying it as it is
not being developed. Instead you should identify a Gateway API
implementation and use it".
The last release, controller-v1.15.1, came out 2026-03-19, Helm chart
4.15.1. Its claimed Kubernetes support is 1.31-1.35, while upstream is
already at 1.37. So on a fresh cluster it's formally outside its
declared support window.
From the joint statement by the Kubernetes Steering Committee and the Security Response Committee: there will be no more releases, bugfixes or security patches, and existing deployments will keep running with unpatched vulnerabilities. The reason is mundane: one or two maintainers working in their spare time and accumulated tech debt. The would-be successor project, InGate, has also been wound down, and there is no direct drop-in replacement.
And yet it hasn't gone anywhere
By the Steering Committee's estimate, ingress-nginx runs in roughly half of cloud-native environments. The controller doesn't self-destruct just because the repository got archived, and it keeps running in production for plenty of teams.
Two conclusions follow. Don't deploy a new ingress-nginx. But reading
and understanding its annotations
(nginx.ingress.kubernetes.io/rewrite-target and the rest) is still
necessary, otherwise you won't be able to make sense of other people's
manifests, and there are a lot of them out there.
An existing controller won't break on its own, but it won't get patches
either, so plan the move. For migrating to Gateway API there's a tool
called ingress2gateway, whose first stable 1.0 release came out in
March 2026, current version v1.2.0.
Check whether it's in your cluster:
kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx
The trap in minikube
minikube addons enable ingress still installs ingress-nginx: in
minikube v1.39.0 that's 1.15.1, in v1.38.1 it was 1.14.3. Issue
kubernetes/minikube#22030, "NGINX retirement: What's the plan for the
ingress addon?", has been open since 2025-12-03. Someone who just
follows the minikube docs ends up deploying a controller with no future
patches and never finds out. Since v1.39.0, minikube has a traefik
addon.
microk8s enable ingress is also nginx-based.
kind is a different story. With cloud-provider-kind v0.9.0, Ingress is supported natively, and kind's documentation says: "No third-party ingress controllers are required by default". cloud-provider-kind (current version v0.12.0) runs as a separate process on the host and needs to keep running as long as you're using Ingress; more on that in the kind lesson.
kind create cluster
kubectl apply -f https://kind.sigs.k8s.io/examples/ingress/usage.yaml
INGRESS_IP=$(kubectl get ingress example-ingress -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl ${INGRESS_IP}/foo
In k3d and k3s, Traefik is installed by default, and you turn it off
with --k3s-arg "--disable=traefik@server:0".
Gateway API
The successor to Ingress, which Kubernetes points to as the way forward. It's a set of CRDs, meaning new resource types (GatewayClass, Gateway, HTTPRoute and others), installed into the cluster separately from the controller.
The current CRD version is v1.6.3 from 2026-10-06. There are two channels:
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.3/standard-install.yaml
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.3/experimental-install.yaml
Standard is the stable set. Experimental adds fields and resources on
top that are still changing: in v1.6.3 that's 13 CRDs against 10 in
Standard. Which channel and version are installed in the cluster is
recorded in the CRD annotations gateway.networking.k8s.io/channel and
gateway.networking.k8s.io/bundle-version.
Why --server-side
A plain kubectl apply works client-side: it puts the whole applied
object into the kubectl.kubernetes.io/last-applied-configuration
annotation. All of an object's annotations together can't exceed
262144 bytes (256 KiB). Gateway API's CRDs are huge: the HTTPRoute
description in Standard v1.6.3 takes up about 420 KiB of YAML, and it
just barely fits in the annotation. Experimental doesn't fit at all,
and the API server rejects the CRD:
The CustomResourceDefinition "httproutes.gateway.networking.k8s.io" is invalid: metadata.annotations: Too long: may not be more than 262144 bytes
Server-side apply stores who owns which fields in managedFields on
the API server's side, and doesn't write that annotation. That's why
the official instructions give --server-side for both channels. For
Standard this is headroom for the future for now; for Experimental
there's no getting around it.
The CRDs themselves don't route anything. After the CRDs are installed, an HTTPRoute can be created, but no traffic will go anywhere until the cluster has an implementation that reads these objects. Locally, these work:
- Traefik Proxy v3.7 supports Standard v1.6.3, turned on with the Helm
value
providers.kubernetesGateway.enabled: true. Convenient for k3d and k3s, where Traefik is already installed. - Envoy Gateway v1.9.2 from 2026-09-28, its chart installs the CRDs
itself:
helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.9.2 -n envoy-gateway-system --create-namespace. - NGINX Gateway Fabric v2.7.2 from 2026-09-16.
- kind supports Gateway API natively via cloud-provider-kind v0.9.0 and above.
StorageClass
Without a StorageClass, any PersistentVolumeClaim with dynamic provisioning stays in Pending. Check it with one command:
kubectl get storageclass
Out of the box, a class exists in k3s and k3d (local-path, the
default), in kind (standard, provisioner rancher.io/local-path) and
in minikube (standard, provisioner k8s.io/minikube-hostpath). The
names differ, even though kind and k3d use the same provisioner under
the hood.
You have to install it by hand in MicroK8s:
microk8s enable hostpath-storage, the class shows up under the name
microk8s-hostpath.
kubeadm, k0s and Talos have no StorageClass at all. The usual fix is to install rancher/local-path-provisioner, current version v0.0.37 from 2026-08-05.
Common mistakes
Running kubectl top right after installing metrics-server.
Metrics don't appear instantly: the Pod has to become Ready and collect
its first readings. If the metrics-server Pod never becomes Ready at
all, check its logs for x509 errors first and remember
--kubelet-insecure-tls.
An Ingress with no controller. An Ingress object gets created in
any cluster, the API accepts it. But the ADDRESS field in
kubectl get ingress stays empty until there's a controller (or
cloud-provider-kind in kind).
Gateway API without --server-side. The Experimental channel only
half-installs with a plain kubectl apply: some CRDs get created,
HTTPRoute doesn't. If you then repeat the install with --server-side,
kubectl may report a field-ownership conflict with
kubectl-client-side-apply.
The wrong StorageClass name in the manifest. A manifest from a k3d
tutorial asks for storageClassName: local-path, but in kind the class
is called standard. The PVC sits in Pending, and its events show
ProvisioningFailed and
storageclass.storage.k8s.io "local-path" not found. A PVC's
storageClassName field can't be changed after creation, so a PVC like
this gets recreated.
The default class and WaitForFirstConsumer. standard in kind uses
WaitForFirstConsumer mode: the PVC stays Pending until some Pod
actually uses it. That's normal, the volume gets created on the node
the scheduler placed the Pod on.
What you'll need for the tasks
Both labs bring up a kind cluster on your own laptop, each needing up to 1.5 GB of Docker memory. You'll need Docker 20.10+, kind v0.31.0 or newer, and kubectl.
- Lab 1:
kubectl get storageclass,kubectl describe pvc,kubectl get events, the "StorageClass" section, and the fact that a PVC can't be changed after creation. - Lab 2: access to github.com from the terminal, the "Gateway API" and
"Why --server-side" sections,
kubectl get crd, and the docs on server-side apply conflicts.
There's no lab for metrics-server: its image isn't part of the course's image set. You can still run the commands above on any kind cluster of your own.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress