Sign in
← Kubernetes locally
10 min · easy · Lesson 7 of 8

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.

Toolmetrics-serverIngressStorageClass
kindinstalled and patched by handnative via cloud-provider-kind v0.9.0+standard
k3s and k3dalready in the clusterTraefiklocal-path
minikubeaddon, no patching neededaddon, ingress-nginx 1.15.1 (minikube v1.39.0)standard
MicroK8saddonaddon, nginx-basedaddon 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:

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.

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.

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.

Get access for $49

Tell the agent: use MCP izi, give me lesson k8s-local-07-addons