Migrate from the retired ingress-nginx controller to Kubernetes Gateway API, and design, debug, and review Gateway API ingress. Covers annotation inventory and risk triage, ingress2gateway conversion, implementation choice (Envoy Gateway default, Cilium, Istio, NGINX Gateway Fabric, AWS Load Balancer Controller), CRD channel and lifecycle, shared-Gateway ownership, cert-manager listener TLS, Flux layering, parallel-run DNS cutover, and rollback. Use when asked to "migrate off ingress-nginx", "convert my Ingresses to HTTPRoute", "set up Gateway API", "why is my HTTPRoute not attached", or "plan the ingress cutover". Flux reconciliation stuck → /platform-skills:gitops. East-west HTTPRoute in a mesh → /platform-skills:linkerd.
95%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
The canonical home for this skill is gateway-api in nitinjain999/platform-skills
Move ingress traffic off the retired ingress-nginx controller onto Gateway API without a user-visible change, and keep a fast rollback until the old path is provably unused.
Read references/gateway-api.md before responding. It holds the implementation matrix, the ownership model, CRD channel rules, the ingress2gateway v1.2.0 annotation coverage table, cert-manager requirements, Flux layering, the seven-phase cutover, rollback by stage, and the troubleshooting table. The working layout is examples/fluxcd/basic-monorepo; a before/after pair for one Ingress is in examples/gateway-api.
This command is interactive by default. With no arguments, ask the three questions below, then run the matching mode. Never apply converted manifests or change DNS yourself: produce the change, the validation, and the rollback, and let the user run it.
Ask one at a time:
assess (have ingress-nginx, not started) / convert (ready to translate) / scaffold (new cluster, no Ingress yet) / cutover (Gateway works, traffic not moved) / debug (a route or Gateway is not working) / review (check existing Gateway API config) / rollback| Mode | What it does | Mutates anything? |
|---|---|---|
assess | Inventories Ingresses, classes and annotations; classifies each annotation as auto, decision, or blocked | No |
convert | Runs ingress2gateway, then rewrites output for the shared-Gateway model | Writes files only |
scaffold | Generates CRDs, controller, GatewayClass, Gateway, cert-manager and Flux layers | Writes files only |
cutover | Produces the per-hostname shadow test, DNS plan, soak criteria and decommission gate | No |
debug | Diagnoses a route, listener, certificate or Gateway address problem | No |
review | Reviews Gateway API manifests for correctness, security and operational safety | No |
rollback | Picks the rollback for the stage reached | No |
kubectl get ingressclass
# One row per Ingress annotation, keeping the resource identity and hosts,
# so every annotation can be traced to the Ingress that carries it.
# Columns: namespace, name, class, hosts, annotation, value. The value is
# kept because several blockers are value-dependent (`rewrite-target: /$1`
# uses a capture group; a snippet's body is what has to be redesigned).
# @tsv escapes tabs and newlines, so a multi-line snippet stays on one row.
# An Ingress with no nginx annotation still gets one row with "-".
kubectl get ingress -A -o json | jq -r '
.items[]
| (.metadata.annotations // {}) as $a
| [.metadata.namespace, .metadata.name,
(.spec.ingressClassName // $a["kubernetes.io/ingress.class"] // "-"),
([.spec.rules[]?.host // "*"] | unique | join(","))] as $id
| ([$a | to_entries[] | select(.key | startswith("nginx.ingress.kubernetes.io/")) | [.key, .value]]
| if length == 0 then [["-", "-"]] else . end)[]
| $id + . | @tsv' > ingress-inventory.tsv
column -t -s $'\t' ingress-inventory.tsv
# Cluster-wide count per annotation, to size the work
kubectl get ingress -A -o json \
| jq -r '.items[] | .metadata.annotations // {} | keys[]' \
| grep '^nginx.ingress.kubernetes.io/' | sort | uniq -c | sort -rn
# Controller-wide settings also change behaviour, not just annotations
kubectl get configmap -A -l app.kubernetes.io/name=ingress-nginx -o yamlreferences/gateway-api.md:
configuration-snippet, server-snippet, auth, rate limiting). Each needs an owner and a design before conversionuse-forwarded-headers, ssl-redirect). They do not appear on any Ingress and are the easiest to loseconvert while any item is blocked without an agreed decisionassess has no open blocked itemsingress2gateway print --providers=ingress-nginx --emitter=envoy-gateway \
-n <namespace> > converted-<namespace>.yaml--emitter=standard for a non-Envoy implementation, and --ingress-nginx-ingress-class=<class> when the class is not nginx. Never pass --allow-experimental-gw-api on a standard-channel clusterGateway objectsparentRefs to platform-gateway in envoy-gateway-system, with the right sectionNameallowedRoutes to the namespace that owns its hostname (kubernetes.io/metadata.name)backendRefsdecision item (filter or policy) and show it next to the original annotationkubeconform -strict -summary \
-schema-location default \
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \
converted-<namespace>.yamlGenerate the layout from examples/fluxcd/basic-monorepo, adapted to the answers:
Kustomization per source with prune: false and deletionPolicy: Orphan: Gateway API config/crd/standard at the pinned tag, plus Envoy Gateway's charts/crds/crds/generated/ from the pinned chart artifact. Set crds.enabled: false on the controller chartminAvailable: 1 on one replicafrom: Same (redirect and ACME only), HTTPS listeners from: Selector bound to the namespace that owns the hostname via kubernetes.io/metadata.name. Never from: All, and never one shared opt-in label across listeners: either lets a team claim another team's hostname. App namespaces are created in the platform layer, never by the app layerRequestRedirect route on port 80 with statusCode: 308config.gatewayAPI.enabled: true, the cert-manager.io/cluster-issuer annotation on the Gateway, and the gatewayHTTPRoute HTTP-01 solver. cert-manager must start after the CRDs exist (its layer dependsOn the CRD Kustomizations)infrastructure → infrastructure-configs → apps) with a healthCheckExprs entry on Gateway Programmed. For Argo CD, sync waves in the same orderbash examples/gateway-api/gateway-api-validate.sh against the adapted copy, or the kubeconform command aboveProduce a per-hostname plan following the migration path in references/gateway-api.md:
Programmed=True, every route Accepted=True and ResolvedRefs=True, every certificate Ready# The address can be an IP or a hostname (cloud LBs often publish a DNS name).
# --connect-to accepts either and keeps the URL host for Host and TLS SNI;
# --resolve only accepts an IP and fails to parse a hostname.
NEW_ADDR=$(kubectl get gateway platform-gateway -n envoy-gateway-system -o jsonpath='{.status.addresses[0].value}')
curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' \
--connect-to <host>:443:"$NEW_ADDR":443 https://<host>/<health-path>Blast radius: one hostname per DNS change. Nothing else moves.
kubectl get gatewayclass
kubectl get gateway -A
kubectl describe gateway <name> -n <namespace>
kubectl get httproute <name> -n <namespace> -o yaml # read .status.parents
kubectl get certificate,certificaterequest -A
kubectl get svc -n envoy-gateway-system
kubectl logs -n envoy-gateway-system deploy/envoy-gateway --tail=100no matches for kind), runtime (route or Gateway conditions)reason and match it to the troubleshooting table in references/gateway-api.md (NotAllowedByListeners, NoMatchingListenerHostname, BackendNotFound, RefNotPermitted, AddressNotAssigned)Review in this order and separate critical findings from improvements:
backendRefs use port numbers; listener hostnames cover route hostnames; every HTTPS listener has hostname, tls.mode: Terminate and certificateRefs; configs depend on CRDsfrom: All on shared listeners; each HTTPS listener binds to the namespace owning its hostname, not a shared label; ReferenceGrants are as narrow as possible; NetworkPolicy admits only proxy pods; no experimental channel unless deliberateProgrammed health checkAsk which stage was reached, then apply the rollback table in references/gateway-api.md:
| Stage | Rollback |
|---|---|
| Before DNS cutover | Delete the new routes; nothing user-facing changed |
| After cutover, ingress-nginx still running | Point DNS back at the ingress-nginx address |
| After decommission | Revert the decommission commit; Flux or Argo re-creates ingress-nginx and the Ingress objects; then switch DNS |
| Bad CRD upgrade | Revert the CRD source tag in Git. Never delete a CRD or its Kustomization |
Every mode ends with:
nitinjain999/platform-skills
c2e75b7
Canonical home
since Oct 12, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.