k8s-moredev Development

Ingress & routing

Your app is published through the platform's shared ingress controller (Traefik). You describe your hostnames and your routes in your repo; DNS and the TLS certificate follow automatically. Because every app shares that one controller, a few rules keep one app from touching another's traffic. A manifest that breaks one of them is rejected at sync time with a message naming the rule - nothing breaks, it just doesn't go live until fixed.

Ingress - the default

Copy ingress.yaml from the example repo. What matters:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod   # TLS certificate
spec:
  ingressClassName: traefik
  tls:
    - hosts: ["my-app.inf.ethz.ch"]
      secretName: my-app-tls
  rules:
    - host: "my-app.inf.ethz.ch"        # required on every rule
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service: {name: my-app, port: {number: 80}}

The DNS record is created for you - don't set an external-dns annotation.

The rules (Ingress, HTTPRoute, Gateway)

HOSTNAMES
  - every Ingress rule needs a `host`; every HTTPRoute needs `hostnames`;
    every Gateway listener needs a `hostname`
  - no wildcards (`*.example.ch`) - list each hostname
  - no `spec.defaultBackend` on an Ingress
  - a hostname belongs to the first namespace that claims it (cluster-wide,
    across Ingress, HTTPRoute and Gateway). Several objects in YOUR namespace
    may share a hostname (e.g. to split paths).

INGRESS ANNOTATIONS
  traefik.ingress.kubernetes.io/   only these four:
      router.entrypoints   web and/or websecure
      router.middlewares   Middlewares of your own namespace (see below)
      router.pathmatcher
      router.tls
    (router.priority, router.tls.options, ... are rejected)
  nginx.ingress.kubernetes.io/     the cluster does not run ingress-nginx.
      These annotations are ignored; a chart that ships them still deploys.
      Rejected outright: *-snippet, auth-url, server-alias,
      from-to-www-redirect, client-body-buffer-size, proxy-buffer-size,
      proxy-buffers-number.

SERVICE ANNOTATIONS
  traefik.ingress.kubernetes.io/   only service.serversscheme,
      service.passhostheader, service.nativelb, service.sticky.*

Middlewares

Redirects, header changes, path rewrites, basic auth, IP allow-lists and rate limits are Traefik Middleware objects you create in your own namespace:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: security-headers
spec:
  headers:
    stsSeconds: 31536000
    customResponseHeaders:
      X-Frame-Options: DENY

Attach it on the Ingress as <namespace>_<name>@kubernetescrd - an underscore between your namespace and the Middleware's name, several separated by commas:

metadata:
  annotations:
    traefik.ingress.kubernetes.io/router.middlewares: my-namespace_security-headers@kubernetescrd
ALLOWED Middleware kinds:
  addPrefix, stripPrefix, stripPrefixRegex, replacePath, replacePathRegex,
  redirectRegex, redirectScheme, headers, contentType, compress,
  encodedCharacters, grpcWeb, basicAuth, digestAuth, passTLSClientCert,
  ipAllowList, rateLimit (in-memory), inFlightReq, circuitBreaker, retry,
  chain (Middlewares of your own namespace only)

NOT ALLOWED (they act with the shared controller's identity or resources):
  forwardAuth, buffering, errors, plugin, rateLimit.redis

Need external authentication in front of your app? Run an auth proxy (e.g. oauth2-proxy) as part of your app instead of forwardAuth.

Rate limiting

The platform only has a coarse safety net: one client IP gets at most 30 requests in flight at a time, anything beyond that is answered 429. If you want real rate limiting - e.g. to keep crawlers from hammering an expensive endpoint such as a search - do that in your own Middlewares:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: search-ratelimit
spec:
  rateLimit:              # per client IP: 2 requests/s on average, bursts of 10
    average: 2
    period: 1s
    burst: 10
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: search-concurrency
spec:
  inFlightReq:            # at most 5 requests in flight in total, from anyone
    amount: 5
    sourceCriterion:
      requestHost: true
  • rateLimit stops a single client. Crawlers that come from many addresses pass it - the inFlightReq cap on the whole endpoint is what keeps the rest of your site fast then: excess requests get a 429 right away instead of queueing up in your app.

  • A Middleware applies to every path of the Ingress it is attached to. To limit one path only, give that path its own Ingress (same host, path: /search) and attach the Middlewares there:

    metadata:
      annotations:
        traefik.ingress.kubernetes.io/router.middlewares: >-
          my-namespace_search-ratelimit@kubernetescrd,my-namespace_search-concurrency@kubernetescrd
    
  • Also list such endpoints in your robots.txt (Disallow: /search) - well-behaved crawlers then never get there.

Gateway API (HTTPRoute)

If you prefer the Gateway API over Ingress (header/method matching, traffic weights, ...), ship a Gateway next to your HTTPRoutes - one listener per hostname:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: my-app
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  gatewayClassName: traefik
  listeners:
    - name: websecure
      protocol: HTTPS
      port: 443
      hostname: my-app.inf.ethz.ch
      allowedRoutes:
        namespaces: {from: Same}
      tls:
        mode: Terminate
        certificateRefs:
          - {group: "", kind: Secret, name: my-app-tls}   # cert-manager creates it
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-app
spec:
  parentRefs:
    - {group: gateway.networking.k8s.io, kind: Gateway, name: my-app}
  hostnames: ["my-app.inf.ethz.ch"]
  rules:
    - backendRefs:
        - {name: my-app, port: 80}
GATEWAY RULES
  gatewayClassName   traefik
  listeners          a literal `hostname` on each; HTTP on port 80 or HTTPS on
                     port 443 only; at most 16
  allowedRoutes      from: Same (your own namespace) - also the default
  tls                mode Terminate, certificateRefs to Secrets in your namespace
  not allowed        spec.addresses, spec.infrastructure, tls.options,
                     ReferenceGrant, GRPCRoute / TCPRoute / TLSRoute / UDPRoute

To use a Middleware from an HTTPRoute, reference it with an ExtensionRef filter (group: traefik.io, kind: Middleware, name: <name>).

Apps that are ETH-internal only

If your app was onboarded as internal (reachable from the ETH network only), it is published on a separate controller:

  • Ingress - nothing to do. Class, entrypoint and certificate issuer are set for you at sync time, whatever your manifest says.
  • Gateway - state it yourself: gatewayClassName: traefik-internal, only HTTPS listeners on port 443, and the issuer letsencrypt-prod-internal.

Requesting an exception

Need something from the "not allowed" lists, a wildcard hostname, or non-HTTP traffic? Open a ticket at support@inf.ethz.ch (what to include) - name the object and field, and why your app needs it.